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

Loading