Skip to content

Add user-callout for disabled web-hooks

Alex Kalderimis requested to merge 362662-user-callout into master

What does this MR do and why?

See: #362662 (closed)

This adds helpers to the Rails code to render a new user callout if any web-hook a user might be responsible for is disabled.

Queries and plans:

The following finders are added:

WebHooks::CalloutsFinder

This is always called as callouts.execute.limit(CALLOUT_BATCH_SIZE).delete_all

The query plans depend on the hook:

With a system hook:

DELETE FROM "user_callouts"
  WHERE "user_callouts"."id" IN (SELECT "user_callouts"."id" FROM "user_callouts" WHERE "user_callouts"."feature_name" = 54 AND "user_callouts"."user_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."admin" = TRUE) LIMIT 100)
Query Plan
ModifyTable on public.user_callouts  (cost=951.37..954.40 rows=1 width=34) (actual time=1592.905..1592.910 rows=0 loops=1)
   Buffers: shared hit=452 read=559 dirtied=30
   I/O Timings: read=1577.366 write=0.000
   ->  Nested Loop  (cost=951.37..954.40 rows=1 width=34) (actual time=1592.830..1592.834 rows=0 loops=1)
         Buffers: shared hit=452 read=559 dirtied=30
         I/O Timings: read=1577.366 write=0.000
         ->  HashAggregate  (cost=950.94..950.95 rows=1 width=32) (actual time=1592.830..1592.832 rows=0 loops=1)
               Group Key: "ANY_subquery".id
               Buffers: shared hit=452 read=559 dirtied=30
               I/O Timings: read=1577.366 write=0.000
               ->  Subquery Scan on ANY_subquery  (cost=0.87..950.94 rows=1 width=32) (actual time=1592.826..1592.828 rows=0 loops=1)
                     Buffers: shared hit=452 read=559 dirtied=30
                     I/O Timings: read=1577.366 write=0.000
                     ->  Limit  (cost=0.87..950.93 rows=1 width=4) (actual time=1592.825..1592.826 rows=0 loops=1)
                           Buffers: shared hit=452 read=559 dirtied=30
                           I/O Timings: read=1577.366 write=0.000
                           ->  Nested Loop  (cost=0.87..950.93 rows=1 width=4) (actual time=1592.823..1592.824 rows=0 loops=1)
                                 Buffers: shared hit=452 read=559 dirtied=30
                                 I/O Timings: read=1577.366 write=0.000
                                 ->  Index Scan using index_users_on_admin on public.users  (cost=0.43..72.12 rows=267 width=4) (actual time=13.020..1072.523 rows=249 loops=1)
                                       Index Cond: (users.admin = true)
                                       Buffers: shared hit=1 read=263 dirtied=30
                                       I/O Timings: read=1066.457 write=0.000
                                 ->  Index Scan using index_user_callouts_on_user_id_and_feature_name on public.user_callouts user_callouts_1  (cost=0.43..3.28 rows=1 width=8) (actual time=2.077..2.077 rows=0 loops=249)
                                       Index Cond: ((user_callouts_1.user_id = users.id) AND (user_callouts_1.feature_name = 54))
                                       Buffers: shared hit=451 read=296
                                       I/O Timings: read=510.909 write=0.000
         ->  Index Scan using user_callouts_pkey on public.user_callouts  (cost=0.43..3.45 rows=1 width=10) (actual time=0.000..0.000 rows=0 loops=0)
               Index Cond: (user_callouts.id = "ANY_subquery".id)
               I/O Timings: read=0.000 write=0.000
Timings
Time: 1.598 s
  - planning: 4.962 ms
  - execution: 1.593 s
    - I/O read: 1.577 s
    - I/O write: 0.000 ms

Shared buffers:
  - hits: 452 (~3.50 MiB) from the buffer pool
  - reads: 559 (~4.40 MiB) from the OS file cache, including disk I/O
  - dirtied: 30 (~240.00 KiB)
  - writes: 0

With a project hook:

DELETE FROM "user_callouts"
 WHERE "user_callouts"."id" IN (
   SELECT "user_callouts"."id" FROM "user_callouts"
    WHERE "user_callouts"."feature_name" = 54
      AND "user_callouts"."user_id" IN (
           SELECT "members"."user_id" FROM "members" LEFT OUTER JOIN "users" ON "users"."id" = "members"."user_id"
            WHERE "members"."type" = 'ProjectMember'
              AND "members"."source_id" = 1
              AND "members"."source_type" = 'Project'
              AND "members"."requested_at" IS NULL
              AND (("members"."user_id" IS NULL AND "members"."invite_token" IS NOT NULL) OR "users"."state" = 'active')
              AND "members"."requested_at" IS NULL
              AND (members.access_level > 5) AND "members"."access_level" IN (50, 40))
    LIMIT 100)
Query Plan
 ModifyTable on public.user_callouts  (cost=31.24..34.27 rows=1 width=34) (actual time=11.640..11.647 rows=0 loops=1)
   Buffers: shared hit=6 read=6
   I/O Timings: read=11.391 write=0.000
   ->  Nested Loop  (cost=31.24..34.27 rows=1 width=34) (actual time=11.637..11.643 rows=0 loops=1)
         Buffers: shared hit=6 read=6
         I/O Timings: read=11.391 write=0.000
         ->  HashAggregate  (cost=30.81..30.82 rows=1 width=32) (actual time=11.636..11.641 rows=0 loops=1)
               Group Key: "ANY_subquery".id
               Buffers: shared hit=6 read=6
               I/O Timings: read=11.391 write=0.000
               ->  Subquery Scan on ANY_subquery  (cost=18.49..30.81 rows=1 width=32) (actual time=11.633..11.637 rows=0 loops=1)
                     Buffers: shared hit=6 read=6
                     I/O Timings: read=11.391 write=0.000
                     ->  Limit  (cost=18.49..30.80 rows=1 width=4) (actual time=11.631..11.635 rows=0 loops=1)
                           Buffers: shared hit=6 read=6
                           I/O Timings: read=11.391 write=0.000
                           ->  Nested Loop  (cost=18.49..30.80 rows=1 width=4) (actual time=11.629..11.632 rows=0 loops=1)
                                 Buffers: shared hit=6 read=6
                                 I/O Timings: read=11.391 write=0.000
                                 ->  HashAggregate  (cost=18.05..18.08 rows=3 width=4) (actual time=11.559..11.562 rows=1 loops=1)
                                       Group Key: members.user_id
                                       Buffers: shared hit=3 read=6
                                       I/O Timings: read=11.391 write=0.000
                                       ->  Nested Loop Left Join  (cost=1.00..18.05 rows=3 width=4) (actual time=11.541..11.549 rows=1 loops=1)
                                             Filter: (((members.user_id IS NULL) AND (members.invite_token IS NOT NULL)) OR ((users.state)::text = 'active'::text))
                                             Rows Removed by Filter: 0
                                             Buffers: shared hit=3 read=6
                                             I/O Timings: read=11.391 write=0.000
                                             ->  Index Scan using index_non_requested_project_members_on_source_id_and_type on public.members  (cost=0.56..7.65 rows=3 width=69) (actual time=7.060..7.065 rows=1 loops=1)
                                                   Index Cond: ((members.source_id = 1) AND ((members.source_type)::text = 'Project'::text))
                                                   Filter: ((members.access_level > 5) AND (members.access_level = ANY ('{50,40}'::integer[])))
                                                   Rows Removed by Filter: 0
                                                   Buffers: shared read=5
                                                   I/O Timings: read=6.964 write=0.000
                                             ->  Index Scan using users_pkey on public.users  (cost=0.43..3.45 rows=1 width=11) (actual time=4.469..4.469 rows=1 loops=1)
                                                   Index Cond: (users.id = members.user_id)
                                                   Buffers: shared hit=3 read=1
                                                   I/O Timings: read=4.427 write=0.000
                                 ->  Index Scan using index_user_callouts_on_user_id_and_feature_name on public.user_callouts user_callouts_1  (cost=0.43..3.29 rows=1 width=8) (actual time=0.062..0.062 rows=0 loops=1)
                                       Index Cond: ((user_callouts_1.user_id = members.user_id) AND (user_callouts_1.feature_name = 54))
                                       Buffers: shared hit=3
                                       I/O Timings: read=0.000 write=0.000
         ->  Index Scan using user_callouts_pkey on public.user_callouts  (cost=0.43..3.45 rows=1 width=10) (actual time=0.000..0.000 rows=0 loops=0)
               Index Cond: (user_callouts.id = "ANY_subquery".id)
               I/O Timings: read=0.000 write=0.000
Timings
Time: 15.495 ms
  - planning: 3.658 ms
  - execution: 11.837 ms
    - I/O read: 11.391 ms
    - I/O write: 0.000 ms

Shared buffers:
  - hits: 6 (~48.00 KiB) from the buffer pool
  - reads: 6 (~48.00 KiB) from the OS file cache, including disk I/O
  - dirtied: 0
  - writes: 0

With a group hook:

DELETE FROM "user_callouts"
 WHERE "user_callouts"."id" IN (
  SELECT "user_callouts"."id" FROM "user_callouts"
   WHERE "user_callouts"."feature_name" = 54
     AND "user_callouts"."user_id" IN (
           SELECT "users"."id" FROM "users" INNER JOIN "members" ON "users"."id" = "members"."user_id"
            WHERE "members"."type" = 'GroupMember'
              AND "members"."source_id" = 22
              AND "members"."source_type" = 'Namespace'
              AND "members"."requested_at" IS NULL
              AND "members"."access_level" != 5
              AND "members"."access_level" = 50
          )
    LIMIT 100)
Query Plan
 ModifyTable on public.user_callouts  (cost=7.99..11.01 rows=1 width=34) (actual time=6.756..6.761 rows=0 loops=1)
   Buffers: shared read=4
   I/O Timings: read=6.590 write=0.000
   ->  Nested Loop  (cost=7.99..11.01 rows=1 width=34) (actual time=6.752..6.757 rows=0 loops=1)
         Buffers: shared read=4
         I/O Timings: read=6.590 write=0.000
         ->  HashAggregate  (cost=7.55..7.56 rows=1 width=32) (actual time=6.752..6.756 rows=0 loops=1)
               Group Key: "ANY_subquery".id
               Buffers: shared read=4
               I/O Timings: read=6.590 write=0.000
               ->  Subquery Scan on ANY_subquery  (cost=7.48..7.55 rows=1 width=32) (actual time=6.749..6.753 rows=0 loops=1)
                     Buffers: shared read=4
                     I/O Timings: read=6.590 write=0.000
                     ->  Limit  (cost=7.48..7.54 rows=1 width=4) (actual time=6.748..6.751 rows=0 loops=1)
                           Buffers: shared read=4
                           I/O Timings: read=6.590 write=0.000
                           ->  Nested Loop  (cost=7.48..7.54 rows=1 width=4) (actual time=6.746..6.749 rows=0 loops=1)
                                 Buffers: shared read=4
                                 I/O Timings: read=6.590 write=0.000
                                 ->  HashAggregate  (cost=7.05..7.06 rows=1 width=8) (actual time=6.745..6.747 rows=0 loops=1)
                                       Group Key: users.id
                                       Buffers: shared read=4
                                       I/O Timings: read=6.590 write=0.000
                                       ->  Nested Loop  (cost=1.00..7.04 rows=1 width=8) (actual time=6.742..6.744 rows=0 loops=1)
                                             Buffers: shared read=4
                                             I/O Timings: read=6.590 write=0.000
                                             ->  Index Scan using index_members_on_source_id_and_source_type on public.members  (cost=0.56..3.59 rows=1 width=4) (actual time=6.740..6.741 rows=0 loops=1)
                                                   Index Cond: ((members.source_id = 22) AND ((members.source_type)::text = 'Namespace'::text))
                                                   Filter: ((members.requested_at IS NULL) AND (members.access_level <> 5) AND ((members.type)::text = 'GroupMember'::text) AND (members.access_level = 50))
                                                   Rows Removed by Filter: 0
                                                   Buffers: shared read=4
                                                   I/O Timings: read=6.590 write=0.000
                                             ->  Index Only Scan using users_pkey on public.users  (cost=0.43..3.45 rows=1 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                                                   Index Cond: (users.id = members.user_id)
                                                   Heap Fetches: 0
                                                   I/O Timings: read=0.000 write=0.000
                                 ->  Index Scan using index_user_callouts_on_user_id_and_feature_name on public.user_callouts user_callouts_1  (cost=0.43..0.46 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=0)
                                       Index Cond: ((user_callouts_1.user_id = users.id) AND (user_callouts_1.feature_name = 54))
                                       I/O Timings: read=0.000 write=0.000
         ->  Index Scan using user_callouts_pkey on public.user_callouts  (cost=0.43..3.45 rows=1 width=10) (actual time=0.000..0.000 rows=0 loops=0)
               Index Cond: (user_callouts.id = "ANY_subquery".id)
               I/O Timings: read=0.000 write=0.000
Timings
Time: 8.854 ms
  - planning: 1.856 ms
  - execution: 6.998 ms
    - I/O read: 6.590 ms
    - I/O write: 0.000 ms

Shared buffers:
  - hits: 0 from the buffer pool
  - reads: 4 (~32.00 KiB) from the OS file cache, including disk I/O
  - dirtied: 0
  - writes: 0

WebHooks::HooksFinder

Called to find hooks a user has access to. Currently only called with any? in the finder to find disabled hooks:

WebHooks::HooksFinder.new(current_user: user, params: { disabled: true }).any?

(using myself as the user, since I have access as a maintainer to a number of web-hooks):

The result of this query is cached in Redis to prevent frequent execution.

SELECT 1 AS one FROM ((SELECT "web_hooks".* FROM "web_hooks" WHERE "web_hooks"."type" = 'ProjectHook' AND "web_hooks"."project_id" IN (SELECT "projects"."id" FROM "projects" INNER JOIN "project_authorizations" ON "projects"."id" = "project_authorizations"."project_id" WHERE "project_authorizations"."user_id" = 3614858 AND (project_authorizations.access_level >= 40) AND "projects"."hidden" = FALSE ORDER BY "projects"."id" DESC))
UNION
(SELECT "web_hooks".* FROM "web_hooks" WHERE "web_hooks"."type" = 'SystemHook')
UNION
(SELECT "web_hooks".* FROM "web_hooks" WHERE "web_hooks"."type" = 'GroupHook' AND "web_hooks"."group_id" IN (WITH RECURSIVE "base_and_descendants" AS ((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" INNER JOIN "members" ON "namespaces"."id" = "members"."source_id" WHERE "members"."type" = 'GroupMember' AND "members"."source_type" = 'Namespace' AND "namespaces"."type" = 'Group' AND "members"."user_id" = 3614858 AND "members"."requested_at" IS NULL AND (access_level >= 10) AND (members.access_level >= 50))
UNION
(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", "base_and_descendants" WHERE "namespaces"."type" = 'Group' AND "namespaces"."parent_id" = "base_and_descendants"."id")) SELECT id FROM "base_and_descendants" AS "namespaces" ORDER BY "namespaces"."id" DESC))) web_hooks WHERE (recent_failures > 3 OR disabled_until >= '2022-06-23 15:51:20.706677') LIMIT 1
Query Plan
Limit  (cost=7407.49..7407.51 rows=1 width=4) (actual time=455.948..455.965 rows=0 loops=1)
   Buffers: shared hit=592 read=239 dirtied=5
   I/O Timings: read=445.243 write=0.000
   ->  Subquery Scan on web_hooks  (cost=7407.49..7408.75 rows=63 width=4) (actual time=455.946..455.962 rows=0 loops=1)
         Buffers: shared hit=592 read=239 dirtied=5
         I/O Timings: read=445.243 write=0.000
         ->  HashAggregate  (cost=7407.49..7408.12 rows=63 width=801) (actual time=455.945..455.959 rows=0 loops=1)
               Group Key: web_hooks_1.id, web_hooks_1.project_id, web_hooks_1.created_at, web_hooks_1.updated_at, web_hooks_1.type, web_hooks_1.service_id, web_hooks_1.push_events, web_hooks_1.issues_events, web_hooks_1.merge_requests_events, web_hooks_1.tag_push_events, web_hooks_1.group_id, web_hooks_1.note_events, web_hooks_1.enable_ssl_verification, web_hooks_1.wiki_page_events, web_hooks_1.pipeline_events, web_hooks_1.confidential_issues_events, web_hooks_1.repository_update_events, web_hooks_1.job_events, web_hooks_1.confidential_note_events, web_hooks_1.push_events_branch_filter, web_hooks_1.encrypted_token, web_hooks_1.encrypted_token_iv, web_hooks_1.encrypted_url, web_hooks_1.encrypted_url_iv, web_hooks_1.deployment_events, web_hooks_1.feature_flag_events, web_hooks_1.releases_events, web_hooks_1.member_events, web_hooks_1.subgroup_events, web_hooks_1.recent_failures, web_hooks_1.backoff_count, web_hooks_1.disabled_until, web_hooks_1.encrypted_url_variables, web_hooks_1.encrypted_url_variables_iv
               Buffers: shared hit=592 read=239 dirtied=5
               I/O Timings: read=445.243 write=0.000
               ->  Append  (cost=2910.32..7402.13 rows=63 width=801) (actual time=455.940..455.955 rows=0 loops=1)
                     Buffers: shared hit=592 read=239 dirtied=5
                     I/O Timings: read=445.243 write=0.000
                     ->  Nested Loop  (cost=2910.32..6296.15 rows=61 width=320) (actual time=378.960..378.965 rows=0 loops=1)
                           Buffers: shared hit=530 read=203 dirtied=4
                           I/O Timings: read=369.797 write=0.000
                           ->  HashAggregate  (cost=2909.90..2917.65 rows=775 width=4) (actual time=309.046..309.183 rows=89 loops=1)
                                 Group Key: projects.id
                                 Buffers: shared hit=280 read=175 dirtied=4
                                 I/O Timings: read=302.052 write=0.000
                                 ->  Nested Loop  (cost=1.01..2900.21 rows=775 width=4) (actual time=20.159..308.554 rows=89 loops=1)
                                       Buffers: shared hit=280 read=175 dirtied=4
                                       I/O Timings: read=302.052 write=0.000
                                       ->  Index Only Scan using project_authorizations_pkey on public.project_authorizations  (cost=0.57..223.63 rows=776 width=4) (actual time=11.135..58.609 rows=89 loops=1)
                                             Index Cond: ((project_authorizations.user_id = 3614858) AND (project_authorizations.access_level >= 40))
                                             Heap Fetches: 9
                                             Buffers: shared hit=61 read=38 dirtied=3
                                             I/O Timings: read=56.706 write=0.000
                                       ->  Index Scan using projects_pkey on public.projects  (cost=0.44..3.45 rows=1 width=4) (actual time=2.802..2.802 rows=1 loops=89)
                                             Index Cond: (projects.id = project_authorizations.project_id)
                                             Filter: (NOT projects.hidden)
                                             Rows Removed by Filter: 0
                                             Buffers: shared hit=219 read=137 dirtied=1
                                             I/O Timings: read=245.346 write=0.000
                           ->  Index Scan using index_web_hooks_on_project_id on public.web_hooks web_hooks_1  (cost=0.43..4.35 rows=1 width=320) (actual time=0.782..0.782 rows=0 loops=89)
                                 Index Cond: (web_hooks_1.project_id = projects.id)
                                 Filter: (((web_hooks_1.type)::text = 'ProjectHook'::text) AND ((web_hooks_1.recent_failures > 3) OR (web_hooks_1.disabled_until >= '2022-06-23 15:51:20.706677+00'::timestamp with time zone)))
                                 Rows Removed by Filter: 0
                                 Buffers: shared hit=250 read=28
                                 I/O Timings: read=67.745 write=0.000
                     ->  Index Scan using index_web_hooks_on_type on public.web_hooks web_hooks_2  (cost=0.43..2.15 rows=1 width=320) (actual time=0.048..0.048 rows=0 loops=1)
                           Index Cond: ((web_hooks_2.type)::text = 'SystemHook'::text)
                           Filter: ((web_hooks_2.recent_failures > 3) OR (web_hooks_2.disabled_until >= '2022-06-23 15:51:20.706677+00'::timestamp with time zone))
                           Rows Removed by Filter: 0
                           Buffers: shared hit=3
                           I/O Timings: read=0.000 write=0.000
                     ->  Nested Loop  (cost=516.17..1102.89 rows=1 width=320) (actual time=76.928..76.934 rows=0 loops=1)
                           Buffers: shared hit=59 read=36 dirtied=1
                           I/O Timings: read=75.447 write=0.000
                           ->  HashAggregate  (cost=515.89..517.89 rows=200 width=4) (actual time=76.799..76.810 rows=8 loops=1)
                                 Group Key: namespaces.id
                                 Buffers: shared hit=43 read=36 dirtied=1
                                 I/O Timings: read=75.447 write=0.000
                                 ->  Sort  (cost=512.58..513.13 rows=221 width=4) (actual time=76.781..76.788 rows=8 loops=1)
                                       Sort Key: namespaces.id DESC
                                       Sort Method: quicksort  Memory: 25kB
                                       Buffers: shared hit=43 read=36 dirtied=1
                                       I/O Timings: read=75.447 write=0.000
                                       CTE base_and_descendants
                                         ->  Recursive Union  (cost=1.13..499.55 rows=221 width=365) (actual time=19.860..76.646 rows=8 loops=1)
                                               Buffers: shared hit=40 read=36 dirtied=1
                                               I/O Timings: read=75.447 write=0.000
                                               ->  Nested Loop  (cost=1.13..12.95 rows=1 width=365) (actual time=19.843..70.478 rows=7 loops=1)
                                                     Buffers: shared hit=18 read=33 dirtied=1
                                                     I/O Timings: read=69.666 write=0.000
                                                     ->  Index Scan using index_members_on_user_id_source_id_source_type on public.members  (cost=0.56..9.36 rows=1 width=4) (actual time=12.923..33.504 rows=7 loops=1)
                                                           Index Cond: ((members.user_id = 3614858) AND ((members.source_type)::text = 'Namespace'::text))
                                                           Filter: ((members.requested_at IS NULL) AND (members.access_level >= 10) AND (members.access_level >= 50) AND ((members.type)::text = 'GroupMember'::text))
                                                           Rows Removed by Filter: 5
                                                           Buffers: shared hit=2 read=14
                                                           I/O Timings: read=33.251 write=0.000
                                                     ->  Index Scan using namespaces_pkey on public.namespaces namespaces_1  (cost=0.56..3.58 rows=1 width=365) (actual time=5.271..5.271 rows=1 loops=7)
                                                           Index Cond: (namespaces_1.id = members.source_id)
                                                           Filter: ((namespaces_1.type)::text = 'Group'::text)
                                                           Rows Removed by Filter: 0
                                                           Buffers: shared hit=16 read=19 dirtied=1
                                                           I/O Timings: read=36.415 write=0.000
                                               ->  Nested Loop  (cost=0.43..48.22 rows=22 width=365) (actual time=2.377..3.021 rows=0 loops=2)
                                                     Buffers: shared hit=22 read=3
                                                     I/O Timings: read=5.780 write=0.000
                                                     ->  WorkTable Scan on base_and_descendants  (cost=0.00..0.20 rows=10 width=4) (actual time=0.003..0.005 rows=4 loops=2)
                                                           I/O Timings: read=0.000 write=0.000
                                                     ->  Index Scan using index_groups_on_parent_id_id on public.namespaces namespaces_2  (cost=0.43..4.78 rows=2 width=365) (actual time=0.751..0.751 rows=0 loops=8)
                                                           Index Cond: (namespaces_2.parent_id = base_and_descendants.id)
                                                           Buffers: shared hit=22 read=3
                                                           I/O Timings: read=5.780 write=0.000
                                       ->  CTE Scan on base_and_descendants namespaces  (cost=0.00..4.42 rows=221 width=4) (actual time=19.865..76.693 rows=8 loops=1)
                                             Buffers: shared hit=40 read=36 dirtied=1
                                             I/O Timings: read=75.447 write=0.000
                           ->  Index Scan using index_web_hooks_on_group_id on public.web_hooks web_hooks_3  (cost=0.28..2.92 rows=1 width=320) (actual time=0.014..0.014 rows=0 loops=8)
                                 Index Cond: (web_hooks_3.group_id = namespaces.id)
                                 Filter: ((web_hooks_3.recent_failures > 3) OR (web_hooks_3.disabled_until >= '2022-06-23 15:51:20.706677+00'::timestamp with time zone))
                                 Rows Removed by Filter: 0
                                 Buffers: shared hit=16
                                 I/O Timings: read=0.000 write=0.000

</details>

<details>
<summary>Timings:</summary>

Time: 460.519 ms

  • planning: 3.919 ms
  • execution: 456.600 ms
    • I/O read: 445.243 ms
    • I/O write: 0.000 ms

Shared buffers:

  • hits: 592 (~4.60 MiB) from the buffer pool
  • reads: 239 (~1.90 MiB) from the OS file cache, including disk I/O
  • dirtied: 5 (~40.00 KiB)
  • writes: 0

</details>

When used to find records, the query is:

```sql
SELECT "web_hooks".* FROM (
  (SELECT "web_hooks".* FROM "web_hooks"
    WHERE "web_hooks"."type" = 'ProjectHook'
      AND "web_hooks"."project_id" IN (
        SELECT "projects"."id" FROM "projects" INNER JOIN "project_authorizations" ON "projects"."id" = "project_authorizations"."project_id"
         WHERE "project_authorizations"."user_id" = 14
           AND (project_authorizations.access_level >= 40)
           AND "projects"."hidden" = FALSE
         ORDER BY "projects"."id" DESC
        )
   )
   
  UNION
  
  (SELECT "web_hooks".* FROM "web_hooks"
    WHERE "web_hooks"."type" = 'GroupHook'
      AND "web_hooks"."group_id" IN (
     WITH
       "descendants_base_cte" AS MATERIALIZED (
         SELECT "namespaces"."traversal_ids", "namespaces"."id" FROM "namespaces" INNER JOIN "members" ON "namespaces"."id" = "members"."source_id"
          WHERE "members"."type" = 'GroupMember'
            AND "members"."source_type" = 'Namespace'
            AND "namespaces"."type" = 'Group' AND "members"."user_id" = 14
            AND "members"."requested_at" IS NULL AND (access_level >= 10) AND (members.access_level >= 50)
        ),
        "superset" AS (
          SELECT d1.traversal_ids FROM descendants_base_cte d1
          WHERE NOT EXISTS (
            SELECT 1
            FROM descendants_base_cte d2
            WHERE d2.id = ANY(d1.traversal_ids)
              AND d2.id <> d1.id
          )
        )
     SELECT DISTINCT id FROM "superset", "namespaces"
      WHERE "namespaces"."type" = 'Group'
        AND next_traversal_ids_sibling("superset"."traversal_ids") > "namespaces"."traversal_ids"
        AND "superset"."traversal_ids" <= "namespaces"."traversal_ids"
        ORDER BY "namespaces"."id" DESC
    )
  )) web_hooks
  WHERE (recent_failures > 3 OR disabled_until >= '2022-06-10 15:58:38')
Query Plan
 HashAggregate  (cost=523050.09..523050.74 rows=65 width=737) (actual time=221.672..221.688 rows=0 loops=1)
   Group Key: web_hooks.id, web_hooks.project_id, web_hooks.created_at, web_hooks.updated_at, web_hooks.type, web_hooks.service_id, web_hooks.push_events, web_hooks.issues_events, web_hooks.merge_requests_events, web_hooks.tag_push_events, web_hooks.group_id, web_hooks.note_events, web_hooks.enable_ssl_verification, web_hooks.wiki_page_events, web_hooks.pipeline_events, web_hooks.confidential_issues_events, web_hooks.repository_update_events, web_hooks.job_events, web_hooks.confidential_note_events, web_hooks.push_events_branch_filter, web_hooks.encrypted_token, web_hooks.encrypted_token_iv, web_hooks.encrypted_url, web_hooks.encrypted_url_iv, web_hooks.deployment_events, web_hooks.feature_flag_events, web_hooks.releases_events, web_hooks.member_events, web_hooks.subgroup_events, web_hooks.recent_failures, web_hooks.backoff_count, web_hooks.disabled_until
   Buffers: shared hit=105 read=114
   I/O Timings: read=219.039 write=0.000
   ->  Append  (cost=3139.57..523044.89 rows=65 width=737) (actual time=221.666..221.681 rows=0 loops=1)
         Buffers: shared hit=105 read=114
         I/O Timings: read=219.039 write=0.000
         ->  Nested Loop  (cost=3139.57..6637.97 rows=64 width=288) (actual time=199.788..199.795 rows=0 loops=1)
               Buffers: shared hit=105 read=104
               I/O Timings: read=197.361 write=0.000
               ->  HashAggregate  (cost=3139.14..3147.15 rows=801 width=4) (actual time=158.824..158.899 rows=28 loops=1)
                     Group Key: projects.id
                     Buffers: shared hit=51 read=74
                     I/O Timings: read=157.021 write=0.000
                     ->  Nested Loop  (cost=1.01..3129.13 rows=801 width=4) (actual time=21.510..158.605 rows=28 loops=1)
                           Buffers: shared hit=51 read=74
                           I/O Timings: read=157.021 write=0.000
                           ->  Index Only Scan using project_authorizations_pkey on public.project_authorizations  (cost=0.57..366.17 rows=801 width=4) (actual time=11.665..13.824 rows=28 loops=1)
                                 Index Cond: ((project_authorizations.user_id = 14) AND (project_authorizations.access_level >= 40))
                                 Heap Fetches: 24
                                 Buffers: shared hit=6 read=7
                                 I/O Timings: read=13.546 write=0.000
                           ->  Index Scan using projects_pkey on public.projects  (cost=0.44..3.45 rows=1 width=4) (actual time=5.163..5.163 rows=1 loops=28)
                                 Index Cond: (projects.id = project_authorizations.project_id)
                                 Filter: (NOT projects.hidden)
                                 Rows Removed by Filter: 0
                                 Buffers: shared hit=45 read=67
                                 I/O Timings: read=143.475 write=0.000
               ->  Index Scan using index_web_hooks_on_project_id on public.web_hooks  (cost=0.43..4.35 rows=1 width=288) (actual time=1.458..1.458 rows=0 loops=28)
                     Index Cond: (web_hooks.project_id = projects.id)
                     Filter: (((web_hooks.type)::text = 'ProjectHook'::text) AND ((web_hooks.recent_failures > 3) OR (web_hooks.disabled_until >= '2022-06-10 15:58:38+00'::timestamp with time zone)))
                     Rows Removed by Filter: 0
                     Buffers: shared hit=54 read=30
                     I/O Timings: read=40.340 write=0.000
         ->  Merge Join  (cost=509817.10..516405.94 rows=1 width=288) (actual time=21.873..21.880 rows=0 loops=1)
               Merge Cond: (namespaces.id = web_hooks_1.group_id)
               Buffers: shared read=10
               I/O Timings: read=21.678 write=0.000
               ->  Unique  (cost=507541.90..509424.11 rows=376442 width=4) (actual time=21.870..21.875 rows=0 loops=1)
                     Buffers: shared read=10
                     I/O Timings: read=21.678 write=0.000
                     CTE descendants_base_cte
                       ->  Nested Loop  (cost=1.13..12.95 rows=1 width=32) (actual time=21.834..21.836 rows=0 loops=1)
                             Buffers: shared read=10
                             I/O Timings: read=21.678 write=0.000
                             ->  Index Scan using index_members_on_user_id_source_id_source_type on public.members  (cost=0.56..9.37 rows=1 width=4) (actual time=21.831..21.832 rows=0 loops=1)
                                   Index Cond: ((members.user_id = 14) AND ((members.source_type)::text = 'Namespace'::text))
                                   Filter: ((members.requested_at IS NULL) AND (members.access_level >= 10) AND (members.access_level >= 50) AND ((members.type)::text = 'GroupMember'::text))
                                   Rows Removed by Filter: 6
                                   Buffers: shared read=10
                                   I/O Timings: read=21.678 write=0.000
                             ->  Index Scan using namespaces_pkey on public.namespaces namespaces_1  (cost=0.56..3.58 rows=1 width=32) (actual time=0.000..0.000 rows=0 loops=0)
                                   Index Cond: (namespaces_1.id = members.source_id)
                                   Filter: ((namespaces_1.type)::text = 'Group'::text)
                                   Rows Removed by Filter: 0
                                   I/O Timings: read=0.000 write=0.000
                     ->  Sort  (cost=507528.95..508470.05 rows=376442 width=4) (actual time=21.868..21.871 rows=0 loops=1)
                           Sort Key: namespaces.id DESC
                           Sort Method: quicksort  Memory: 25kB
                           Buffers: shared read=10
                           I/O Timings: read=21.678 write=0.000
                           ->  Nested Loop  (cost=0.80..472666.52 rows=376442 width=4) (actual time=21.841..21.843 rows=0 loops=1)
                                 Buffers: shared read=10
                                 I/O Timings: read=21.678 write=0.000
                                 ->  Nested Loop Anti Join  (cost=0.00..0.07 rows=1 width=32) (actual time=21.840..21.840 rows=0 loops=1)
                                       Buffers: shared read=10
                                       I/O Timings: read=21.678 write=0.000
                                       ->  CTE Scan on descendants_base_cte d1  (cost=0.00..0.02 rows=1 width=36) (actual time=21.838..21.838 rows=0 loops=1)
                                             Buffers: shared read=10
                                             I/O Timings: read=21.678 write=0.000
                                       ->  CTE Scan on descendants_base_cte d2  (cost=0.00..0.02 rows=1 width=4) (actual time=0.000..0.000 rows=0 loops=0)
                                             I/O Timings: read=0.000 write=0.000
                                 ->  Index Scan using index_namespaces_on_traversal_ids_for_groups_btree on public.namespaces  (cost=0.80..468902.04 rows=376442 width=32) (actual time=0.000..0.000 rows=0 loops=0)
                                       Index Cond: ((namespaces.traversal_ids < next_traversal_ids_sibling(d1.traversal_ids)) AND (namespaces.traversal_ids >= d1.traversal_ids))
                                       I/O Timings: read=0.000 write=0.000
               ->  Sort  (cost=2275.21..2275.75 rows=219 width=288) (actual time=0.000..0.000 rows=0 loops=0)
                     Sort Key: web_hooks_1.group_id DESC
                     I/O Timings: read=0.000 write=0.000
                     ->  Index Scan using index_web_hooks_on_type on public.web_hooks web_hooks_1  (cost=0.43..2266.69 rows=219 width=288) (actual time=0.000..0.000 rows=0 loops=0)
                           Index Cond: ((web_hooks_1.type)::text = 'GroupHook'::text)
                           Filter: ((web_hooks_1.recent_failures > 3) OR (web_hooks_1.disabled_until >= '2022-06-10 15:58:38+00'::timestamp with time zone))
                           Rows Removed by Filter: 0
                           I/O Timings: read=0.000 write=0.000
Timings:
Time: 236.422 ms
  - planning: 12.702 ms
  - execution: 223.720 ms
    - I/O read: 219.039 ms
    - I/O write: 0.000 ms

Shared buffers:
  - hits: 105 (~840.00 KiB) from the buffer pool
  - reads: 114 (~912.00 KiB) from the OS file cache, including disk I/O
  - dirtied: 0
  - writes: 0

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Alex Kalderimis

Merge request reports