Move code owner group loading to db layer

What does this MR do and why?

Simplifies the group loading logic for code owners.

Issues in old implementation:
  • applicable_ancestors called ActiveRecord::Relation#select with a block to match values which behaves like Array#select causing the logic to iterate over the whole collection.
  • Called applicable_ancestors twice without memoizing method.
  • Converting group_list to array and then calling uniq before returning.
Changes:
  • Now we use Group.from_union which automatically removes duplicates, removing the need for us to iterate over group_list and applicable_ancestors
  • applicable_ancestors method completely removed, less debt and also no need to memoize.
  • Added feature_category: :source_code_management to all code owner specs

Benchmark locally

Benchmarked locally with the following code.
N = 1000
module Before
  module_function

  def load_groups(project, extractor)
    return Group.none if extractor.names.empty?

    groups = project.invited_groups.where_full_path_in(extractor.names);

    group_list = groups.with_route.with_users.to_a;

    if project.group
      # If the project.group's ancestor group(s) are listed as owners, add
      #   them to group_list
      #
      if applicable_ancestors(project, extractor.names).any?
        group_list.concat(applicable_ancestors(project, extractor.names));
      end
    end

    group_list.uniq;
  end

  def applicable_ancestors(project, extractor_names)
    ancestor_groups = project.group.self_and_ancestors.with_route.with_users;

    ancestor_groups.select { |group| extractor_names.include?(group.full_path) };
  end
end

module After
  module_function

  def load_groups(project, extractor)
    return Group.none if extractor.names.empty?

    relations = [
      project.invited_groups.where_full_path_in(extractor.names, use_includes: false)
    ];
    # Include the projects ancestor group(s) if they are listed as owners
    if project.group
      relations << project.group.self_and_ancestors.where_full_path_in(extractor.names, use_includes: false);
    end

    Group.from_union(relations).with_route.with_users;
  end
end

Benchmark.bm do |bm|
  project = Project.last
  extractor = OpenStruct.new(names: ['gitlab-org', 'toolbox', 'Commit451'])
  bm.report('load_groups before: ') { N.times { Before.load_groups(project, extractor) } }
  bm.report('load_groups after: ') { N.times { After.load_groups(project, extractor) } }
end
                      user       system     total    real
load_groups before:   8.112644   0.148106   8.260750 ( 10.382684)
load_groups after:    0.666265   0.004052   0.670317 (  0.671757)

Query Diff

Before

project.invited_groups.where_full_path_in(extractor.names).with_route.with_users

SELECT 
  "namespaces"."id" AS t0_r0, 
  "namespaces"."name" AS t0_r1, 
  "namespaces"."path" AS t0_r2, 
  "namespaces"."owner_id" AS t0_r3, 
  "namespaces"."created_at" AS t0_r4, 
  "namespaces"."updated_at" AS t0_r5, 
  "namespaces"."type" AS t0_r6, 
  "namespaces"."description" AS t0_r7, 
  "namespaces"."avatar" AS t0_r8, 
  "namespaces"."membership_lock" AS t0_r9, 
  "namespaces"."share_with_group_lock" AS t0_r10, 
  "namespaces"."visibility_level" AS t0_r11, 
  "namespaces"."request_access_enabled" AS t0_r12, 
  "namespaces"."ldap_sync_status" AS t0_r13, 
  "namespaces"."ldap_sync_error" AS t0_r14, 
  "namespaces"."ldap_sync_last_update_at" AS t0_r15, 
  "namespaces"."ldap_sync_last_successful_update_at" AS t0_r16, 
  "namespaces"."ldap_sync_last_sync_at" AS t0_r17, 
  "namespaces"."description_html" AS t0_r18, 
  "namespaces"."lfs_enabled" AS t0_r19, 
  "namespaces"."parent_id" AS t0_r20, 
  "namespaces"."shared_runners_minutes_limit" AS t0_r21, 
  "namespaces"."repository_size_limit" AS t0_r22, 
  "namespaces"."require_two_factor_authentication" AS t0_r23, 
  "namespaces"."two_factor_grace_period" AS t0_r24, 
  "namespaces"."cached_markdown_version" AS t0_r25, 
  "namespaces"."project_creation_level" AS t0_r26, 
  "namespaces"."runners_token" AS t0_r27, 
  "namespaces"."file_template_project_id" AS t0_r28, 
  "namespaces"."saml_discovery_token" AS t0_r29, 
  "namespaces"."runners_token_encrypted" AS t0_r30, 
  "namespaces"."custom_project_templates_group_id" AS t0_r31, 
  "namespaces"."auto_devops_enabled" AS t0_r32, 
  "namespaces"."extra_shared_runners_minutes_limit" AS t0_r33, 
  "namespaces"."last_ci_minutes_notification_at" AS t0_r34, 
  "namespaces"."last_ci_minutes_usage_notification_level" AS t0_r35, 
  "namespaces"."subgroup_creation_level" AS t0_r36, 
  "namespaces"."emails_disabled" AS t0_r37, 
  "namespaces"."max_pages_size" AS t0_r38, 
  "namespaces"."max_artifacts_size" AS t0_r39, 
  "namespaces"."mentions_disabled" AS t0_r40, 
  "namespaces"."default_branch_protection" AS t0_r41, 
  "namespaces"."unlock_membership_to_ldap" AS t0_r42, 
  "namespaces"."max_personal_access_token_lifetime" AS t0_r43, 
  "namespaces"."push_rule_id" AS t0_r44, 
  "namespaces"."shared_runners_enabled" AS t0_r45, 
  "namespaces"."allow_descendants_override_disabled_shared_runners" AS t0_r46, 
  "namespaces"."traversal_ids" AS t0_r47, 
  "routes"."id" AS t1_r0, 
  "routes"."source_id" AS t1_r1, 
  "routes"."source_type" AS t1_r2, 
  "routes"."path" AS t1_r3, 
  "routes"."created_at" AS t1_r4, 
  "routes"."updated_at" AS t1_r5, 
  "routes"."name" AS t1_r6, 
  "routes"."namespace_id" AS t1_r7, 
  "users"."id" AS t2_r0, 
  "users"."email" AS t2_r1, 
  "users"."encrypted_password" AS t2_r2, 
  "users"."reset_password_token" AS t2_r3, 
  "users"."reset_password_sent_at" AS t2_r4, 
  "users"."remember_created_at" AS t2_r5, 
  "users"."sign_in_count" AS t2_r6, 
  "users"."current_sign_in_at" AS t2_r7, 
  "users"."last_sign_in_at" AS t2_r8, 
  "users"."current_sign_in_ip" AS t2_r9, 
  "users"."last_sign_in_ip" AS t2_r10, 
  "users"."created_at" AS t2_r11, 
  "users"."updated_at" AS t2_r12, 
  "users"."name" AS t2_r13, 
  "users"."admin" AS t2_r14, 
  "users"."projects_limit" AS t2_r15, 
  "users"."failed_attempts" AS t2_r16, 
  "users"."locked_at" AS t2_r17, 
  "users"."username" AS t2_r18, 
  "users"."can_create_group" AS t2_r19, 
  "users"."can_create_team" AS t2_r20, 
  "users"."state" AS t2_r21, 
  "users"."color_scheme_id" AS t2_r22, 
  "users"."password_expires_at" AS t2_r23, 
  "users"."created_by_id" AS t2_r24, 
  "users"."last_credential_check_at" AS t2_r25, 
  "users"."avatar" AS t2_r26, 
  "users"."confirmation_token" AS t2_r27, 
  "users"."confirmed_at" AS t2_r28, 
  "users"."confirmation_sent_at" AS t2_r29, 
  "users"."unconfirmed_email" AS t2_r30, 
  "users"."hide_no_ssh_key" AS t2_r31, 
  "users"."admin_email_unsubscribed_at" AS t2_r32, 
  "users"."notification_email" AS t2_r33, 
  "users"."hide_no_password" AS t2_r34, 
  "users"."password_automatically_set" AS t2_r35, 
  "users"."encrypted_otp_secret" AS t2_r36, 
  "users"."encrypted_otp_secret_iv" AS t2_r37, 
  "users"."encrypted_otp_secret_salt" AS t2_r38, 
  "users"."otp_required_for_login" AS t2_r39, 
  "users"."otp_backup_codes" AS t2_r40, 
  "users"."public_email" AS t2_r41, 
  "users"."dashboard" AS t2_r42, 
  "users"."project_view" AS t2_r43, 
  "users"."consumed_timestep" AS t2_r44, 
  "users"."layout" AS t2_r45, 
  "users"."hide_project_limit" AS t2_r46, 
  "users"."note" AS t2_r47, 
  "users"."unlock_token" AS t2_r48, 
  "users"."otp_grace_period_started_at" AS t2_r49, 
  "users"."external" AS t2_r50, 
  "users"."incoming_email_token" AS t2_r51, 
  "users"."auditor" AS t2_r52, 
  "users"."require_two_factor_authentication_from_group" AS t2_r53, 
  "users"."two_factor_grace_period" AS t2_r54, 
  "users"."last_activity_on" AS t2_r55, 
  "users"."notified_of_own_activity" AS t2_r56, 
  "users"."preferred_language" AS t2_r57, 
  "users"."email_opted_in" AS t2_r58, 
  "users"."email_opted_in_ip" AS t2_r59, 
  "users"."email_opted_in_source_id" AS t2_r60, 
  "users"."email_opted_in_at" AS t2_r61, 
  "users"."theme_id" AS t2_r62, 
  "users"."accepted_term_id" AS t2_r63, 
  "users"."feed_token" AS t2_r64, 
  "users"."private_profile" AS t2_r65, 
  "users"."roadmap_layout" AS t2_r66, 
  "users"."include_private_contributions" AS t2_r67, 
  "users"."commit_email" AS t2_r68, 
  "users"."group_view" AS t2_r69, 
  "users"."managing_group_id" AS t2_r70, 
  "users"."first_name" AS t2_r71, 
  "users"."last_name" AS t2_r72, 
  "users"."static_object_token" AS t2_r73, 
  "users"."role" AS t2_r74, 
  "users"."user_type" AS t2_r75, 
  "users"."static_object_token_encrypted" AS t2_r76, 
  "users"."otp_secret_expires_at" AS t2_r77, 
  "users"."onboarding_in_progress" AS t2_r78 
FROM 
  "namespaces" 
  INNER JOIN "project_group_links" ON "namespaces"."id" = "project_group_links"."group_id" 
  LEFT OUTER JOIN "routes" ON "routes"."source_type" = 'Namespace' 
  AND "routes"."source_id" = "namespaces"."id" 
  LEFT OUTER JOIN "members" ON "members"."source_type" = 'Namespace' 
  AND "members"."requested_at" IS NULL 
  AND "members"."access_level" != 5 
  AND "members"."source_id" = "namespaces"."id" 
  AND "members"."type" = 'GroupMember' 
  LEFT OUTER JOIN "users" ON "users"."id" = "members"."user_id" 
WHERE 
  "namespaces"."type" = 'Group' 
  AND "project_group_links"."project_id" = 21 
  AND (
    (
      LOWER(routes.path) = LOWER('gitlab-org')
    ) 
    OR (
      LOWER(routes.path) = LOWER('toolbox')
    ) 
    OR (
      LOWER(routes.path) = LOWER('Commit451')
    )
  )

project.group.self_and_ancestors.with_route.with_users (performed twice 🙁)

Group Load (0.5ms)  SELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = 29 LIMIT 1 

Group Load (0.3ms)  SELECT "namespaces".* FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = 29 

Route Load (0.2ms)  SELECT "routes".* FROM "routes" WHERE "routes"."source_type" = 'Namespace' AND "routes"."source_id" = 29 

GroupMember Load (0.3ms)  SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Namespace' AND "members"."type" = 'GroupMember' AND "members"."requested_at" IS NULL AND "members"."access_level" != 5 AND "members"."source_id" = 29

User Load (0.6ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 10, 11, 4, 2)

After

Group.from_union([project.invited_groups.where_full_path_in(extractor.names, use_includes: false), project.group.self_and_ancestors.where_full_path_in(extractor.names, use_includes: false)]).with_route.with_users

Group Load (1.5ms) 
SELECT 
  "namespaces".* 
FROM 
  (
    (
      SELECT 
        "namespaces".* 
      FROM 
        "namespaces" 
        INNER JOIN "project_group_links" ON "namespaces"."id" = "project_group_links"."group_id" 
        INNER JOIN "routes" ON "routes"."source_type" = 'Namespace' 
        AND "routes"."source_id" = "namespaces"."id" 
      WHERE 
        "namespaces"."type" = 'Group' 
        AND "project_group_links"."project_id" = 21 
        AND (
          (
            LOWER(routes.path) = LOWER('gitlab-org')
          ) 
          OR (
            LOWER(routes.path) = LOWER('toolbox')
          ) 
          OR (
            LOWER(routes.path) = LOWER('Commit451')
          )
        )
    ) 
    UNION 
      (
        SELECT 
          "namespaces".* 
        FROM 
          "namespaces" 
          INNER JOIN "routes" ON "routes"."source_type" = 'Namespace' 
          AND "routes"."source_id" = "namespaces"."id" 
        WHERE 
          "namespaces"."type" = 'Group' 
          AND "namespaces"."id" = 29 
          AND (
            (
              LOWER(routes.path) = LOWER('gitlab-org')
            ) 
            OR (
              LOWER(routes.path) = LOWER('toolbox')
            ) 
            OR (
              LOWER(routes.path) = LOWER('Commit451')
            )
          )
      )
  ) namespaces 
WHERE 
  "namespaces"."type" = 'Group'

Route Load (0.3ms)  SELECT "routes".* FROM "routes" WHERE "routes"."source_type" = 'Namespace' AND "routes"."source_id" IN (22, 24, 29)

GroupMember Load (0.3ms) 
SELECT 
  "members".* 
FROM 
  "members" 
WHERE 
  "members"."source_type" = 'Namespace' 
  AND "members"."type" = 'GroupMember' 
  AND "members"."requested_at" IS NULL 
  AND "members"."access_level" != 5 
  AND "members"."source_id" IN (22, 24, 29)

User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 10, 11, 4, 2, 17, 6, 9, 14, 12)

MR acceptance checklist

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

Part of #219916 (closed)

Edited by Joe Woodward

Merge request reports

Loading