Simplify instance-level integration propagation batching
What does this MR do and why?
Instance-level propagation passes the without_integration anti-join straight into each_batch, so every boundary probe re-runs NOT EXISTS until it finds 10k matches. For projects in the Default organization that's about 5.2 s per probe.
Behind simplified_instance_propagation_batching, this batches over in_organization and lets the workers re-filter each id range, like !234568 (merged) did for group hierarchies. With the flag off the batching is unchanged.
The workers now load the batch once instead of running a LIMIT 1 probe first and then to_a. That's one query less per job, and empty windows can't land on a fast-start LIMIT 1 plan. This part isn't behind the flag.
Trade-off: re-saving an instance integration now enqueues one job per 10k projects/groups in the organization, even if they already have it (about 5,340 + 1,113 for org 1). Most of them find nothing to create and exit after one bounded query.
References
- #631695 (closed), rollout issue #632633
- Same fix for group hierarchies: #591182 (closed), !234568 (merged), !238401 (merged)
Screenshots or screen recordings
N/A, backend only.
How to set up and validate locally
- In the Rails console, run
Feature.enable(:simplified_instance_propagation_batching). - Create an instance-level Jira integration in the Admin area.
- Check that integrations are created only for projects/groups that didn't have one.
Database
gitlab-production-main, organization 1. All new queries are Index Only Scans on index_projects_on_organization_id_and_id / index_namespaces_on_organization_id_and_id_for_groups. The worker load is a Nested Loop Anti Join with one unique index probe per row, so it never scans Jira rows from the start of the integrations index.
| Query | Before | After |
|---|---|---|
| Projects boundary probe | ~5.2 s | 484 ms (mostly cold reads, 324 heap fetches for 10k rows) |
| Groups boundary probe (start id 71835418) | 3,553 ms | 143 ms (75 heap fetches for 10k rows) |
| Start query, projects / groups | not measured | 4 ms / 5 ms |
| MIN/MAX pick, projects / groups | not measured | 0.2 ms / 0.07 ms |
| Worker load, projects / groups | LIMIT 1 probe + this same query |
12.2 s / 3.0 s, this query only |
Before numbers are from #631695 (closed). The worker load is the same query BulkCreateService#to_a runs today, and almost all of its time is cold-cache reads.
Start query, projects
SELECT "projects"."id" FROM "projects" WHERE "projects"."organization_id" = 1 ORDER BY "projects"."id" ASC LIMIT 1;https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/62841/commands/168053
Limit (cost=0.56..0.59 rows=1 width=4) (actual time=3.827..3.828 rows=1 loops=1)
Buffers: shared hit=3 read=5
I/O Timings: read=3.749 write=0.000
-> Index Only Scan using index_projects_on_organization_id_and_id on public.projects (cost=0.56..1294733.35 rows=53398676 width=4) (actual time=3.826..3.826 rows=1 loops=1)
Index Cond: (projects.organization_id = 1)
Heap Fetches: 0
Buffers: shared hit=3 read=5
I/O Timings: read=3.749 write=0.000
Settings: effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4'
Query ID: -3906590343970485723Start query, groups
SELECT "namespaces"."id" FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."organization_id" = 1 ORDER BY "namespaces"."id" ASC LIMIT 1;https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/62841/commands/168054
Limit (cost=0.56..0.59 rows=1 width=4) (actual time=5.119..5.120 rows=1 loops=1)
Buffers: shared hit=3 read=5
I/O Timings: read=5.034 write=0.000
-> Index Only Scan using index_namespaces_on_organization_id_and_id_for_groups on public.namespaces (cost=0.56..376795.76 rows=11127717 width=4) (actual time=5.118..5.118 rows=1 loops=1)
Index Cond: (namespaces.organization_id = 1)
Heap Fetches: 0
Buffers: shared hit=3 read=5
I/O Timings: read=5.034 write=0.000
Settings: seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5'
Query ID: -8255785235287734268Boundary probe, projects
SELECT "projects"."id" FROM "projects" WHERE "projects"."organization_id" = 1 AND (projects.id >= 40000000) ORDER BY "projects"."id" ASC LIMIT 1 OFFSET 10000;https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/62841/commands/168055
Limit (cost=269.07..269.10 rows=1 width=4) (actual time=483.702..483.704 rows=1 loops=1)
Buffers: shared hit=9639 read=429 dirtied=134
WAL: records=142 fpi=134 bytes=1023680
I/O Timings: read=473.194 write=0.000
-> Index Only Scan using index_projects_on_organization_id_and_id on public.projects (cost=0.56..744325.22 rows=27720962 width=4) (actual time=7.253..483.239 rows=10001 loops=1)
Index Cond: ((projects.organization_id = 1) AND (projects.id >= 40000000))
Heap Fetches: 324
Buffers: shared hit=9639 read=429 dirtied=134
WAL: records=142 fpi=134 bytes=1023680
I/O Timings: read=473.194 write=0.000
Settings: seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5'
Query ID: -6805742817194606375Boundary probe, groups
SELECT "namespaces"."id" FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."organization_id" = 1 AND (namespaces.id >= 71835418) ORDER BY "namespaces"."id" ASC LIMIT 1 OFFSET 10000;https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/62841/commands/168056
Limit (cost=387.69..387.73 rows=1 width=4) (actual time=143.267..143.268 rows=1 loops=1)
Buffers: shared hit=4082 read=186 dirtied=18
WAL: records=20 fpi=18 bytes=144122
I/O Timings: read=139.972 write=0.000
-> Index Only Scan using index_namespaces_on_organization_id_and_id_for_groups on public.namespaces (cost=0.56..200024.24 rows=5166876 width=4) (actual time=11.552..142.843 rows=10001 loops=1)
Index Cond: ((namespaces.organization_id = 1) AND (namespaces.id >= 71835418))
Heap Fetches: 75
Buffers: shared hit=4082 read=186 dirtied=18
WAL: records=20 fpi=18 bytes=144122
I/O Timings: read=139.972 write=0.000
Settings: seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5'
Query ID: -8004001202366326083MIN/MAX pick, projects
SELECT MIN(projects.id), MAX(projects.id) FROM "projects" WHERE "projects"."organization_id" = 1 AND (projects.id >= 40000000) AND (projects.id < 40020000);https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/62841/commands/168057
Result (cost=1.19..1.20 rows=1 width=8) (actual time=0.245..0.246 rows=1 loops=1)
Buffers: shared hit=13 read=1
I/O Timings: read=0.139 write=0.000
InitPlan 1
-> Limit (cost=0.56..0.60 rows=1 width=4) (actual time=0.068..0.068 rows=1 loops=1)
Buffers: shared hit=9
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_projects_on_organization_id_and_id on public.projects (cost=0.56..375.32 rows=11889 width=4) (actual time=0.067..0.067 rows=1 loops=1)
Index Cond: ((projects.organization_id = 1) AND (projects.id >= 40000000) AND (projects.id < 40020000))
Heap Fetches: 1
Buffers: shared hit=9
I/O Timings: read=0.000 write=0.000
InitPlan 2
-> Limit (cost=0.56..0.60 rows=1 width=4) (actual time=0.174..0.174 rows=1 loops=1)
Buffers: shared hit=4 read=1
I/O Timings: read=0.139 write=0.000
-> Index Only Scan Backward using index_projects_on_organization_id_and_id on public.projects projects_1 (cost=0.56..375.32 rows=11889 width=4) (actual time=0.174..0.174 rows=1 loops=1)
Index Cond: ((projects_1.organization_id = 1) AND (projects_1.id >= 40000000) AND (projects_1.id < 40020000))
Heap Fetches: 0
Buffers: shared hit=4 read=1
I/O Timings: read=0.139 write=0.000
Settings: seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5'
Query ID: -3799318338413263752MIN/MAX pick, groups
SELECT MIN(namespaces.id), MAX(namespaces.id) FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."organization_id" = 1 AND (namespaces.id >= 71835418) AND (namespaces.id < 71935418);https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/62841/commands/168058
Result (cost=1.22..1.23 rows=1 width=8) (actual time=0.073..0.074 rows=1 loops=1)
Buffers: shared hit=13
I/O Timings: read=0.000 write=0.000
InitPlan 1
-> Limit (cost=0.56..0.61 rows=1 width=4) (actual time=0.049..0.049 rows=1 loops=1)
Buffers: shared hit=8
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_namespaces_on_organization_id_and_id_for_groups on public.namespaces (cost=0.56..341.24 rows=7108 width=4) (actual time=0.048..0.048 rows=1 loops=1)
Index Cond: ((namespaces.organization_id = 1) AND (namespaces.id >= 71835418) AND (namespaces.id < 71935418))
Heap Fetches: 0
Buffers: shared hit=8
I/O Timings: read=0.000 write=0.000
InitPlan 2
-> Limit (cost=0.56..0.61 rows=1 width=4) (actual time=0.022..0.022 rows=1 loops=1)
Buffers: shared hit=5
I/O Timings: read=0.000 write=0.000
-> Index Only Scan Backward using index_namespaces_on_organization_id_and_id_for_groups on public.namespaces namespaces_1 (cost=0.56..341.24 rows=7108 width=4) (actual time=0.022..0.022 rows=1 loops=1)
Index Cond: ((namespaces_1.organization_id = 1) AND (namespaces_1.id >= 71835418) AND (namespaces_1.id < 71935418))
Heap Fetches: 0
Buffers: shared hit=5
I/O Timings: read=0.000 write=0.000
Settings: seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5'
Query ID: -1671751587520921056Worker load, projects
SELECT "projects".* FROM "projects" LEFT OUTER JOIN "namespaces" ON "namespaces"."type" = 'Group' AND "namespaces"."id" = "projects"."namespace_id" AND "namespaces"."type" = 'Group' WHERE "projects"."id" BETWEEN 40000000 AND 40020000 AND (NOT EXISTS (SELECT 1 FROM "integrations" WHERE (integrations.project_id = projects.id) AND "integrations"."type_new" = 'Integrations::Jira')) AND "projects"."pending_delete" = FALSE AND "projects"."archived" = FALSE AND NOT (EXISTS (SELECT 1 FROM "namespace_settings" WHERE "namespace_settings"."namespace_id" = ANY (namespaces.traversal_ids) AND "namespace_settings"."archived" = TRUE)) AND "projects"."organization_id" = 1;https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/62841/commands/168064
Gather (cost=1001.98..58755.49 rows=10857 width=745) (actual time=28.926..12233.709 rows=9858 loops=1)
Workers Planned: 1
Workers Launched: 1
Buffers: shared hit=101939 read=23336 dirtied=62
WAL: records=65 fpi=62 bytes=471886
I/O Timings: read=23987.650 write=0.000
-> Nested Loop Anti Join (cost=1.98..56669.79 rows=6386 width=745) (actual time=24.069..12178.187 rows=4929 loops=2)
Buffers: shared hit=101939 read=23336 dirtied=62
WAL: records=65 fpi=62 bytes=471886
I/O Timings: read=23987.650 write=0.000
-> Nested Loop Left Join (cost=1.69..52800.21 rows=6434 width=773) (actual time=22.742..12106.087 rows=4960 loops=2)
Buffers: shared hit=85211 read=23228 dirtied=62
WAL: records=65 fpi=62 bytes=471886
I/O Timings: read=23901.721 write=0.000
-> Nested Loop Anti Join (cost=1.12..29907.95 rows=6434 width=745) (actual time=13.715..6325.206 rows=4960 loops=2)
Buffers: shared hit=47149 read=11687 dirtied=40
WAL: records=41 fpi=40 bytes=295780
I/O Timings: read=12471.073 write=0.000
-> Parallel Index Scan using idx_projects_on_repository_storage_last_repository_updated_at on public.projects (cost=0.56..16334.39 rows=6763 width=745) (actual time=5.301..6240.640 rows=5737 loops=2)
Index Cond: ((projects.id >= 40000000) AND (projects.id <= 40020000))
Filter: ((NOT projects.pending_delete) AND (NOT projects.archived) AND (projects.organization_id = 1))
Rows Removed by Filter: 305
Buffers: shared hit=640 read=11626 dirtied=38
WAL: records=39 fpi=38 bytes=279646
I/O Timings: read=12399.233 write=0.000
-> Index Only Scan using index_integrations_on_project_id_and_type_new_unique on public.integrations (cost=0.56..2.01 rows=1 width=4) (actual time=0.012..0.012 rows=0 loops=11474)
Index Cond: ((integrations.project_id = projects.id) AND (integrations.type_new = 'Integrations::Jira'::text))
Heap Fetches: 18
Buffers: shared hit=46509 read=61 dirtied=2
WAL: records=2 fpi=2 bytes=16134
I/O Timings: read=71.839 write=0.000
-> Index Scan using namespaces_pkey on public.namespaces (cost=0.57..3.56 rows=1 width=32) (actual time=1.164..1.164 rows=1 loops=9920)
Index Cond: (namespaces.id = projects.namespace_id)
Filter: ((namespaces.type)::text = 'Group'::text)
Buffers: shared hit=38061 read=11541 dirtied=21
WAL: records=23 fpi=21 bytes=168637
I/O Timings: read=11430.648 write=0.000
-> Index Only Scan using index_namespace_settings_on_namespace_id_where_archived_true on public.namespace_settings (cost=0.29..0.60 rows=1 width=4) (actual time=0.013..0.013 rows=0 loops=9920)
Index Cond: (namespace_settings.namespace_id = ANY (namespaces.traversal_ids))
Heap Fetches: 2
Buffers: shared hit=16728 read=108
I/O Timings: read=85.929 write=0.000
Settings: seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5'
Query ID: -6054932044437869409Worker load, groups
SELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" BETWEEN 71835418 AND 71935418 AND (NOT EXISTS (SELECT 1 FROM "integrations" WHERE (integrations.group_id = namespaces.id) AND "integrations"."type_new" = 'Integrations::Jira')) AND "namespaces"."organization_id" = 1;https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/62841/commands/168065
Gather (cost=1001.12..15086.77 rows=7074 width=366) (actual time=8.497..3024.935 rows=8292 loops=1)
Workers Planned: 1
Workers Launched: 1
Buffers: shared hit=37362 read=6721 dirtied=4
WAL: records=6 fpi=4 bytes=32044
I/O Timings: read=5934.635 write=0.000
-> Nested Loop Anti Join (cost=1.12..13379.37 rows=4161 width=366) (actual time=5.533..3008.845 rows=4146 loops=2)
Buffers: shared hit=37362 read=6721 dirtied=4
WAL: records=6 fpi=4 bytes=32044
I/O Timings: read=5934.635 write=0.000
-> Parallel Index Scan using index_namespaces_on_organization_id_and_id_for_groups on public.namespaces (cost=0.56..5240.32 rows=4181 width=366) (actual time=0.807..2959.833 rows=4392 loops=2)
Index Cond: ((namespaces.organization_id = 1) AND (namespaces.id >= 71835418) AND (namespaces.id <= 71935418))
Buffers: shared hit=1903 read=6692
I/O Timings: read=5882.257 write=0.000
-> Index Only Scan using index_integrations_on_unique_group_id_and_type_new on public.integrations (cost=0.56..1.95 rows=1 width=8) (actual time=0.010..0.010 rows=0 loops=8784)
Index Cond: ((integrations.group_id = namespaces.id) AND (integrations.type_new = 'Integrations::Jira'::text))
Heap Fetches: 11
Buffers: shared hit=35459 read=29 dirtied=4
WAL: records=6 fpi=4 bytes=32044
I/O Timings: read=52.379 write=0.000
Settings: work_mem = '230MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4', effective_cache_size = '472585MB'
Query ID: -6374369254462350497MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.
Related to #631695 (closed)