Scope instance-level integration lookups to organization

What does this MR do and why?

Instance-level integrations (instance = true) now belong to one organization. Lookups filter by organization_id, so an org never picks up another org's defaults, and two orgs can each have their own Jira.

  • find_or_initialize_non_project_specific_integration takes organization_id: for instance rows, and raises when it's missing
  • default_integration, default_integrations and inherited_descendants_from_self_or_ancestors_from only look at the owner's org
  • new for_organization scope, used by the admin integrations page and Project#integration_instances
  • type uniqueness is scoped to organization_id, matching the unique index from !258698 (merged)
  • add organization_level?, drop the unused instance_exists_for?
  • admin overrides only lists projects from the current org

for_instance stays for now. Its other callers move in #631289 (closed).

No feature flag. Self-managed has a single org, so nothing changes there.

Database

GitLab.com has no instance rows today, so the instance parts come back empty. The plans mostly show which index gets picked.

Query Where it runs Plan
Admin overrides, default org Admin::IntegrationsController#overrides plan, 72 ms, cold cache
Admin overrides, small custom org (3000409) Admin::IntegrationsController#overrides plan, 3 ms, starts from index_projects_on_organization_id_and_id
default_integrations union project and group create and transfer plan, 13 ms, cold cache, instance part uses index_integrations_on_type_new_and_organization_id_unique
instance_level_integration default_integration fallback plan, 0.016 ms, uses index_integrations_on_type_new_and_organization_id_unique
Admin overrides plan, default org
 Limit  (cost=1.14..39.99 rows=20 width=749) (actual time=22.937..72.428 rows=20 loops=1)
   Buffers: shared hit=37 read=68
   I/O Timings: read=59.950 write=0.000
   ->  Nested Loop  (cost=1.14..4759884.03 rows=2450278 width=749) (actual time=22.935..72.417 rows=20 loops=1)
         Buffers: shared hit=37 read=68
         I/O Timings: read=59.950 write=0.000
         ->  Index Scan using index_integrations_on_type_new on public.integrations  (cost=0.56..623379.59 rows=2450286 width=4) (actual time=7.772..7.810 rows=20 loops=1)
               Index Cond: (integrations.type_new = 'Integrations::Jira'::text)
               Filter: (integrations.active AND ((integrations.inherit_from_id IS NULL) OR (integrations.inherit_from_id <> 1)))
               Buffers: shared read=5
               I/O Timings: read=7.726 write=0.000
         ->  Memoize  (cost=0.57..1.75 rows=1 width=749) (actual time=3.227..3.227 rows=1 loops=20)
               Cache Key: integrations.project_id
               Cache Mode: logical
               Hits: 0  Misses: 20  Evictions: 0  Overflows: 0  Memory Usage: 13kB
               Buffers: shared hit=37 read=63
               I/O Timings: read=52.224 write=0.000
               ->  Index Scan using projects_pkey on public.projects  (cost=0.56..1.74 rows=1 width=749) (actual time=2.630..2.630 rows=1 loops=20)
                     Index Cond: (projects.id = integrations.project_id)
                     Filter: (projects.organization_id = 1)
                     Buffers: shared hit=37 read=63
                     I/O Timings: read=52.224 write=0.000
Settings: work_mem = '230MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4', effective_cache_size = '472585MB'
Query ID: -2804112599988663296
Admin overrides plan, small custom org
  Limit  (cost=1.12..778.52 rows=8 width=749) (actual time=2.950..2.951 rows=0 loops=1)
   Buffers: shared hit=3 read=4
   I/O Timings: read=2.901 write=0.000
   ->  Nested Loop  (cost=1.12..778.52 rows=8 width=749) (actual time=2.948..2.949 rows=0 loops=1)
         Buffers: shared hit=3 read=4
         I/O Timings: read=2.901 write=0.000
         ->  Index Scan using index_projects_on_organization_id_and_id on public.projects  (cost=0.56..140.84 rows=178 width=749) (actual time=2.947..2.948 rows=0 loops=1)
               Index Cond: (projects.organization_id = 3000409)
               Buffers: shared hit=3 read=4
               I/O Timings: read=2.901 write=0.000
         ->  Index Scan using index_integrations_on_project_id_and_type_new_unique on public.integrations  (cost=0.56..3.58 rows=1 width=4) (never executed)
               Index Cond: ((integrations.project_id = projects.id) AND (integrations.type_new = 'Integrations::Jira'::text))
               Filter: (integrations.active AND ((integrations.inherit_from_id IS NULL) OR (integrations.inherit_from_id <> 1)))
               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: -2804112599988663296
default_integrations plan
 Sort  (cost=13.93..13.94 rows=2 width=267) (actual time=13.297..13.300 rows=1 loops=1)
   Sort Key: integrations.type_new, (array_position('{2750817,9970}'::bigint[], integrations.group_id)), integrations.instance DESC
   Sort Method: quicksort  Memory: 25kB
   Buffers: shared hit=41 read=12
   I/O Timings: read=13.042 write=0.000
   ->  Subquery Scan on integrations  (cost=13.74..13.92 rows=2 width=267) (actual time=13.266..13.270 rows=1 loops=1)
         Buffers: shared hit=30 read=12
         I/O Timings: read=13.042 write=0.000
         ->  Unique  (cost=13.74..13.92 rows=2 width=263) (actual time=13.257..13.260 rows=1 loops=1)
               Buffers: shared hit=30 read=12
               I/O Timings: read=13.042 write=0.000
               ->  Sort  (cost=13.74..13.74 rows=2 width=263) (actual time=13.255..13.257 rows=1 loops=1)
                     Sort Key: integrations_1.id, integrations_1.project_id, integrations_1.created_at, integrations_1.updated_at, integrations_1.active, integrations_1.push_events, integrations_1.issues_events, integrations_1.merge_requests_events, integrations_1.tag_push_events, integrations_1.note_events, integrations_1.category, integrations_1.wiki_page_events, integrations_1.pipeline_events, integrations_1.confidential_issues_events, integrations_1.commit_events, integrations_1.job_events, integrations_1.confidential_note_events, integrations_1.deployment_events, integrations_1.comment_on_event_enabled, integrations_1.instance, integrations_1.comment_detail, integrations_1.inherit_from_id, integrations_1.alert_events, integrations_1.group_id, integrations_1.type_new, integrations_1.vulnerability_events, integrations_1.archive_trace_events, integrations_1.encrypted_properties, integrations_1.encrypted_properties_iv, integrations_1.incident_events, integrations_1.group_mention_events, integrations_1.group_confidential_mention_events, integrations_1.organization_id, integrations_1.event_filters, integrations_1.filter
                     Sort Method: quicksort  Memory: 25kB
                     Buffers: shared hit=30 read=12
                     I/O Timings: read=13.042 write=0.000
                     ->  Append  (cost=0.12..13.73 rows=2 width=263) (actual time=7.329..13.217 rows=1 loops=1)
                           Buffers: shared hit=13 read=12
                           I/O Timings: read=13.042 write=0.000
                           ->  Index Scan using index_integrations_on_type_new_and_organization_id_unique on public.integrations integrations_1  (cost=0.12..3.14 rows=1 width=427) (actual time=0.001..0.001 rows=0 loops=1)
                                 Index Cond: (integrations_1.organization_id = 1)
                                 Filter: (integrations_1.active AND (integrations_1.type_new <> ALL ('{Integrations::AmazonQ,Integrations::BeyondIdentity}'::text[])))
                                 Buffers: shared hit=1
                                 I/O Timings: read=0.000 write=0.000
                           ->  Nested Loop  (cost=1.13..10.58 rows=1 width=427) (actual time=7.326..13.212 rows=1 loops=1)
                                 Buffers: shared hit=12 read=12
                                 I/O Timings: read=13.042 write=0.000
                                 ->  Index Only Scan using index_namespaces_on_type_and_id on public.namespaces  (cost=0.57..5.67 rows=1 width=4) (actual time=4.067..6.745 rows=2 loops=1)
                                       Index Cond: ((namespaces.type = 'Group'::text) AND (namespaces.id = ANY ('{9970,2750817}'::integer[])))
                                       Heap Fetches: 0
                                       Buffers: shared hit=2 read=8
                                       I/O Timings: read=6.674 write=0.000
                                 ->  Index Scan using index_integrations_on_unique_group_id_and_type_new on public.integrations integrations_2  (cost=0.56..4.91 rows=1 width=427) (actual time=3.226..3.227 rows=0 loops=2)
                                       Index Cond: (integrations_2.group_id = namespaces.id)
                                       Filter: (integrations_2.active AND (integrations_2.inherit_from_id IS NULL) AND (integrations_2.type_new <> ALL ('{Integrations::AmazonQ,Integrations::BeyondIdentity}'::text[])))
                                       Rows Removed by Filter: 1
                                       Buffers: shared hit=10 read=4
                                       I/O Timings: read=6.368 write=0.000
Settings: jit = 'off', random_page_cost = '1.5', seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB'
Query ID: 3966530287687146193
instance_level_integration plan
 Limit  (cost=0.12..3.14 rows=1 width=427) (actual time=0.016..0.016 rows=0 loops=1)
   Buffers: shared hit=5
   I/O Timings: read=0.000 write=0.000
   ->  Index Scan using index_integrations_on_type_new_and_organization_id_unique on public.integrations  (cost=0.12..3.14 rows=1 width=427) (actual time=0.015..0.015 rows=0 loops=1)
         Index Cond: ((integrations.type_new = 'Integrations::Jira'::text) AND (integrations.organization_id = 1))
         Buffers: shared hit=5
         I/O Timings: read=0.000 write=0.000
Settings: work_mem = '230MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4', effective_cache_size = '472585MB'
Query ID: -3243719665763061232

No plans for the rest, since nothing really changes for them:

  • inherited_descendants_from_self_or_ancestors_from already had the OR instance = true branch. Adding organization_id only narrows it.
  • for_organization and find_or_initialize_non_project_specific_integration are the same kind of lookup as instance_level_integration, on the same partial unique index.
  • The type uniqueness check has no instance = true, so it can't use the partial index. It's still cheap, since only instance rows have organization_id set (index_integrations_on_organization_id).

References

Related to #631287 (closed)

Edited by Rez

Merge request reports

Loading
Loading