Skip to content

GraphQL: Fix N+1 issues with RunnerGroupsResolver

What does this MR do and why?

This MR adds support for preloading relationships for fullPath and webUrl properties for CiRunner.groups, which are in use for the Runners' admin UI.

For this, it was necessary to create a RunnerGroupsResolver, similar to RunnerProjectsResolver.

Part of #384066 (closed) and #385355 (closed)

How to set up and validate locally

Numbered steps to set up and validate the change are strongly suggested.

  1. Visit http://gdk.test:3000/-/graphql-explorer

  2. Run the following query:

    {
      runners(type: GROUP_TYPE, last: 20) {
        count
        nodes {
          id
          runnerType
          groups {
            nodes {
              id
              path
              fullPath
              webUrl
            }
          }
        }
      }
    }

The queries generated by this branch should no longer generate a route query per-runner.

The functionality isn't changed in this MR, we only avoid an N+1 issue which is covered by the test.

MR acceptance checklist

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

Database queries

Old queries
Processing by GraphqlController#execute as */*
  Parameters: {"query"=>"{\n  runners(type: GROUP_TYPE, last: 20) {\n    count\n    nodes {\n      id\n      runnerType\n      groups {\n        nodes {\n          id\n          fullPath\n          webUrl\n        }\n      }\n    }\n  }\n}\n", "variables"=>"[FILTERED]", "graphql"=>{"query"=>"{\n  runners(type: GROUP_TYPE, last: 20) {\n    count\n    nodes {\n      id\n      runnerType\n      groups {\n        nodes {\n          id\n          fullPath\n          webUrl\n        }\n      }\n    }\n  }\n}\n", "variables"=>"[FILTERED]"}}
   (14.6ms)  SELECT COUNT(*) FROM "ci_runners" WHERE "ci_runners"."runner_type" = 2 /*application:web,correlation_id:01GM3GT53083526YXFRBSK9KYP,endpoint_id:graphql:unknown,db_config_name:ci,line:/app/graphql/types/countable_connection_type.rb:20:in `count'*/
  ↳ app/graphql/types/countable_connection_type.rb:20:in `count'
  Ci::Runner Load (2.9ms)  SELECT "ci_runners".* FROM "ci_runners" WHERE "ci_runners"."runner_type" = 2 ORDER BY "ci_runners"."created_at" ASC, "ci_runners"."id" ASC LIMIT 21 /*application:web,correlation_id:01GM3GT53083526YXFRBSK9KYP,endpoint_id:graphql:unknown,db_config_name:ci,line:/lib/gitlab/graphql/pagination/keyset/connection.rb:114:in `block in limited_nodes'*/
  ↳ lib/gitlab/graphql/pagination/keyset/connection.rb:114:in `block in limited_nodes'
   (0.4ms)  SELECT "ci_runner_namespaces"."runner_id", "ci_runner_namespaces"."namespace_id" FROM "ci_runner_namespaces" WHERE "ci_runner_namespaces"."runner_id" IN (3246, 3245, 3244, 3243, 3242, 3241, 3240, 3239, 3238, 3237, 3236, 3235, 3234, 3233, 3232, 3231, 3230, 3229, 3228, 7) /*application:web,correlation_id:01GM3GT53083526YXFRBSK9KYP,endpoint_id:GraphqlController#execute,db_config_name:ci,line:/app/graphql/types/ci/runner_type.rb:164:in `block in batched_owners'*/
  ↳ app/graphql/types/ci/runner_type.rb:164:in `block in batched_owners'
  Group Load (1.3ms)  SELECT "namespaces"."id", "namespaces"."name", "namespaces"."path", "namespaces"."owner_id", "namespaces"."created_at", "namespaces"."updated_at", "namespaces"."type", "namespaces"."description", "namespaces"."avatar", "namespaces"."membership_lock", "namespaces"."share_with_group_lock", "namespaces"."visibility_level", "namespaces"."request_access_enabled", "namespaces"."ldap_sync_status", "namespaces"."ldap_sync_error", "namespaces"."ldap_sync_last_update_at", "namespaces"."ldap_sync_last_successful_update_at", "namespaces"."ldap_sync_last_sync_at", "namespaces"."description_html", "namespaces"."lfs_enabled", "namespaces"."parent_id", "namespaces"."shared_runners_minutes_limit", "namespaces"."repository_size_limit", "namespaces"."require_two_factor_authentication", "namespaces"."two_factor_grace_period", "namespaces"."cached_markdown_version", "namespaces"."project_creation_level", "namespaces"."runners_token", "namespaces"."file_template_project_id", "namespaces"."saml_discovery_token", "namespaces"."runners_token_encrypted", "namespaces"."custom_project_templates_group_id", "namespaces"."auto_devops_enabled", "namespaces"."extra_shared_runners_minutes_limit", "namespaces"."last_ci_minutes_notification_at", "namespaces"."last_ci_minutes_usage_notification_level", "namespaces"."subgroup_creation_level", "namespaces"."emails_disabled", "namespaces"."max_pages_size", "namespaces"."max_artifacts_size", "namespaces"."mentions_disabled", "namespaces"."default_branch_protection", "namespaces"."unlock_membership_to_ldap", "namespaces"."max_personal_access_token_lifetime", "namespaces"."push_rule_id", "namespaces"."shared_runners_enabled", "namespaces"."allow_descendants_override_disabled_shared_runners", "namespaces"."traversal_ids" FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" IN (27, 22) /*application:web,correlation_id:01GM3GT53083526YXFRBSK9KYP,endpoint_id:GraphqlController#execute,db_config_name:main,line:/app/graphql/types/ci/runner_type.rb:172:in `block in batched_owners'*/
   app/graphql/types/ci/runner_type.rb:172:in `block in batched_owners'
  License Load (0.2ms)  SELECT "licenses".* FROM "licenses" ORDER BY "licenses"."id" DESC LIMIT 100 /*application:web,correlation_id:01GM3GT53083526YXFRBSK9KYP,endpoint_id:GraphqlController#execute,db_config_name:main,line:/ee/app/models/license.rb:82:in `load_license'*/
  ↳ ee/app/models/license.rb:82:in `load_license'
  IpRestriction Load (0.2ms)  SELECT "ip_restrictions".* FROM "ip_restrictions" WHERE "ip_restrictions"."group_id" = 22 /*application:web,correlation_id:01GM3GT53083526YXFRBSK9KYP,endpoint_id:GraphqlController#execute,db_config_name:main,line:/ee/lib/gitlab/ip_restriction/enforcer.rb:31:in `allows_address?'*/
   ee/lib/gitlab/ip_restriction/enforcer.rb:31:in `allows_address?'
  Route Load (0.5ms)  SELECT "routes".* FROM "routes" WHERE "routes"."source_id" = 22 AND "routes"."source_type" = 'Namespace' LIMIT 1 /*application:web,correlation_id:01GM3GT53083526YXFRBSK9KYP,endpoint_id:GraphqlController#execute,db_config_name:main,line:/app/models/concerns/routable.rb:121:in `full_path'*/
  ↳ app/models/concerns/routable.rb:121:in `full_path'
  IpRestriction Load (0.2ms)  SELECT "ip_restrictions".* FROM "ip_restrictions" WHERE "ip_restrictions"."group_id" = 27 /*application:web,correlation_id:01GM3GT53083526YXFRBSK9KYP,endpoint_id:GraphqlController#execute,db_config_name:main,line:/ee/lib/gitlab/ip_restriction/enforcer.rb:31:in `allows_address?'*/
   ee/lib/gitlab/ip_restriction/enforcer.rb:31:in `allows_address?'
  Route Load (0.2ms)  SELECT "routes".* FROM "routes" WHERE "routes"."source_id" = 27 AND "routes"."source_type" = 'Namespace' LIMIT 1 /*application:web,correlation_id:01GM3GT53083526YXFRBSK9KYP,endpoint_id:GraphqlController#execute,db_config_name:main,line:/app/models/concerns/routable.rb:121:in `full_path'*/
  ↳ app/models/concerns/routable.rb:121:in `full_path'
Completed 200 OK in 203ms (Views: 0.4ms | ActiveRecord: 39.9ms | Elasticsearch: 0.0ms | Allocations: 193796)
New queries
Processing by GraphqlController#execute as */*
  Parameters: {"query"=>"{\n  runners(type: GROUP_TYPE, last: 20) {\n    count\n    nodes {\n      id\n      runnerType\n      groups {\n        nodes {\n          id\n          fullPath\n          webUrl\n        }\n      }\n    }\n  }\n}\n", "variables"=>"[FILTERED]", "graphql"=>{"query"=>"{\n  runners(type: GROUP_TYPE, last: 20) {\n    count\n    nodes {\n      id\n      runnerType\n      groups {\n        nodes {\n          id\n          fullPath\n          webUrl\n        }\n      }\n    }\n  }\n}\n", "variables"=>"[FILTERED]"}}
   (7.9ms)  SELECT COUNT(*) FROM "ci_runners" WHERE "ci_runners"."runner_type" = 2 /*application:web,correlation_id:01GM3GVSADWSKFH16VDGVN1HK3,endpoint_id:graphql:unknown,db_config_name:ci,line:/app/graphql/types/countable_connection_type.rb:20:in `count'*/
  ↳ app/graphql/types/countable_connection_type.rb:20:in `count'
  Ci::Runner Load (0.6ms)  SELECT "ci_runners".* FROM "ci_runners" WHERE "ci_runners"."runner_type" = 2 ORDER BY "ci_runners"."created_at" ASC, "ci_runners"."id" ASC LIMIT 21 /*application:web,correlation_id:01GM3GVSADWSKFH16VDGVN1HK3,endpoint_id:graphql:unknown,db_config_name:ci,line:/lib/gitlab/graphql/pagination/keyset/connection.rb:114:in `block in limited_nodes'*/
  ↳ lib/gitlab/graphql/pagination/keyset/connection.rb:114:in `block in limited_nodes'
   (0.3ms)  SELECT "ci_runner_namespaces"."runner_id", "ci_runner_namespaces"."namespace_id" FROM "ci_runner_namespaces" WHERE "ci_runner_namespaces"."runner_id" IN (3246, 3245, 3244, 3243, 3242, 3241, 3240, 3239, 3238, 3237, 3236, 3235, 3234, 3233, 3232, 3231, 3230, 3229, 3228, 7) /*application:web,correlation_id:01GM3GVSADWSKFH16VDGVN1HK3,endpoint_id:GraphqlController#execute,db_config_name:ci,line:/app/graphql/resolvers/ci/runner_groups_resolver.rb:24:in `block in resolve_with_lookahead'*/
  ↳ app/graphql/resolvers/ci/runner_groups_resolver.rb:24:in `block in resolve_with_lookahead'
  Group Load (0.3ms)  SELECT "namespaces"."id", "namespaces"."name", "namespaces"."path", "namespaces"."owner_id", "namespaces"."created_at", "namespaces"."updated_at", "namespaces"."type", "namespaces"."description", "namespaces"."avatar", "namespaces"."membership_lock", "namespaces"."share_with_group_lock", "namespaces"."visibility_level", "namespaces"."request_access_enabled", "namespaces"."ldap_sync_status", "namespaces"."ldap_sync_error", "namespaces"."ldap_sync_last_update_at", "namespaces"."ldap_sync_last_successful_update_at", "namespaces"."ldap_sync_last_sync_at", "namespaces"."description_html", "namespaces"."lfs_enabled", "namespaces"."parent_id", "namespaces"."shared_runners_minutes_limit", "namespaces"."repository_size_limit", "namespaces"."require_two_factor_authentication", "namespaces"."two_factor_grace_period", "namespaces"."cached_markdown_version", "namespaces"."project_creation_level", "namespaces"."runners_token", "namespaces"."file_template_project_id", "namespaces"."saml_discovery_token", "namespaces"."runners_token_encrypted", "namespaces"."custom_project_templates_group_id", "namespaces"."auto_devops_enabled", "namespaces"."extra_shared_runners_minutes_limit", "namespaces"."last_ci_minutes_notification_at", "namespaces"."last_ci_minutes_usage_notification_level", "namespaces"."subgroup_creation_level", "namespaces"."emails_disabled", "namespaces"."max_pages_size", "namespaces"."max_artifacts_size", "namespaces"."mentions_disabled", "namespaces"."default_branch_protection", "namespaces"."unlock_membership_to_ldap", "namespaces"."max_personal_access_token_lifetime", "namespaces"."push_rule_id", "namespaces"."shared_runners_enabled", "namespaces"."allow_descendants_override_disabled_shared_runners", "namespaces"."traversal_ids" FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" IN (27, 22) /*application:web,correlation_id:01GM3GVSADWSKFH16VDGVN1HK3,endpoint_id:GraphqlController#execute,db_config_name:main,line:/app/models/preloaders/user_max_access_level_in_groups_preloader.rb:48:in `map'*/
   app/models/preloaders/user_max_access_level_in_groups_preloader.rb:48:in `map'
  Route Load (0.2ms)  SELECT "routes".* FROM "routes" WHERE "routes"."source_type" = 'Namespace' AND "routes"."source_id" IN (22, 27) /*application:web,correlation_id:01GM3GVSADWSKFH16VDGVN1HK3,endpoint_id:GraphqlController#execute,db_config_name:main,line:/app/models/preloaders/user_max_access_level_in_groups_preloader.rb:48:in `map'*/
  ↳ app/models/preloaders/user_max_access_level_in_groups_preloader.rb:48:in `map'
   (0.8ms)  SELECT MAX("members"."access_level") AS maximum_access_level, "hierarchy"."id" AS hierarchy_id FROM "members" LEFT OUTER JOIN "users" ON "users"."id" = "members"."user_id" INNER JOIN (SELECT id, unnest(traversal_ids) as traversal_id FROM "namespaces" WHERE "namespaces"."id" IN (22, 27)) as hierarchy ON members.source_id = hierarchy.traversal_id WHERE "members"."type" = 'GroupMember' AND "members"."source_type" = 'Namespace' AND "users"."state" = 'active' AND "members"."state" = 0 AND "members"."requested_at" IS NULL AND "members"."invite_token" IS NULL AND (members.access_level > 5) AND "members"."user_id" = 1 GROUP BY "hierarchy"."id" /*application:web,correlation_id:01GM3GVSADWSKFH16VDGVN1HK3,endpoint_id:GraphqlController#execute,db_config_name:main,line:/app/models/preloaders/user_max_access_level_in_groups_preloader.rb:39:in `preload_with_traversal_ids'*/
   app/models/preloaders/user_max_access_level_in_groups_preloader.rb:39:in `preload_with_traversal_ids'
  Namespace Load (0.5ms)  SELECT namespaces.*, root_query.id as source_id FROM "namespaces" INNER JOIN (SELECT id, traversal_ids[1] as root_id FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" IN (22, 27)) as root_query ON root_query.root_id = namespaces.id /*application:web,correlation_id:01GM3GVSADWSKFH16VDGVN1HK3,endpoint_id:GraphqlController#execute,db_config_name:main,line:/app/models/preloaders/group_root_ancestor_preloader.rb:19:in `group_by'*/
  ↳ app/models/preloaders/group_root_ancestor_preloader.rb:19:in `group_by'
  IpRestriction Load (0.2ms)  SELECT "ip_restrictions".* FROM "ip_restrictions" WHERE "ip_restrictions"."group_id" IN (22, 27) /*application:web,correlation_id:01GM3GVSADWSKFH16VDGVN1HK3,endpoint_id:GraphqlController#execute,db_config_name:main,line:/app/models/preloaders/group_root_ancestor_preloader.rb:19:in `group_by'*/
   app/models/preloaders/group_root_ancestor_preloader.rb:19:in `group_by'
  SamlProvider Load (0.2ms)  SELECT "saml_providers".* FROM "saml_providers" WHERE "saml_providers"."group_id" IN (22, 27) /*application:web,correlation_id:01GM3GVSADWSKFH16VDGVN1HK3,endpoint_id:GraphqlController#execute,db_config_name:main,line:/app/models/preloaders/group_root_ancestor_preloader.rb:19:in `group_by'*/
  ↳ app/models/preloaders/group_root_ancestor_preloader.rb:19:in `group_by'
  IpRestriction Load (0.1ms)  SELECT "ip_restrictions".* FROM "ip_restrictions" WHERE "ip_restrictions"."group_id" = 22 /*application:web,correlation_id:01GM3GVSADWSKFH16VDGVN1HK3,endpoint_id:GraphqlController#execute,db_config_name:main,line:/ee/lib/gitlab/ip_restriction/enforcer.rb:31:in `allows_address?'*/
   ee/lib/gitlab/ip_restriction/enforcer.rb:31:in `allows_address?'
  IpRestriction Load (0.2ms)  SELECT "ip_restrictions".* FROM "ip_restrictions" WHERE "ip_restrictions"."group_id" = 27 /*application:web,correlation_id:01GM3GVSADWSKFH16VDGVN1HK3,endpoint_id:GraphqlController#execute,db_config_name:main,line:/ee/lib/gitlab/ip_restriction/enforcer.rb:31:in `allows_address?'*/
  ↳ ee/lib/gitlab/ip_restriction/enforcer.rb:31:in `allows_address?'
Completed 200 OK in 581ms (Views: 0.5ms | ActiveRecord: 11.3ms | Elasticsearch: 0.0ms | Allocations: 81387)
Edited by Alper Akgun

Merge request reports