Skip to content

Add pipeline status to GraphQL projects query

Peter Hegman requested to merge add-pipline-status-to-graphql-query into master

What does this MR do and why?

Related to #482803

In &13066 we are moving Your work -> Projects to Vue and using GraphQL as the data source. We need to be able to get the pipeline status for projects from the GraphQL query but this is not possible at the moment. This MR adds a lastest_pipeline_detailed_status field that returns detailed pipeline status for the latest pipeline.

Originally I was going to just expose project.pipeline_status but then I realized the cached pipeline status does not use ci/status/pipeline/factory.rb#L9 so we would miss out on status such as Status::SuccessWarning which we need. I also realized it is more standard to expose app/graphql/types/ci/detailed_status_type.rb and that has more helpful information. Because of these requirements I settled on project.lastestPipelineDetailedStatus

To limit the number of Gitaly calls we repurpose the same cache used in the HAML version so Gitaly calls are only made if the pipeline status is not cached.

MR acceptance checklist

Please evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Database review

Query 1

SQL

Click to expand
SELECT DISTINCT ON (sha, project_id)
    *
FROM
    "p_ci_pipelines"
WHERE ("p_ci_pipelines"."project_id" = 7750063
    AND "p_ci_pipelines"."sha" = 'ddc336dd390fdad2f5abb353eac4230b15f7ac98'
    OR "p_ci_pipelines"."project_id" = 7071551
    AND "p_ci_pipelines"."sha" = 'c98665605248a130e825326db99ddaf96a49b929'
    OR "p_ci_pipelines"."project_id" = 278964
    AND "p_ci_pipelines"."sha" = '946c03839c36348ece7243e1d08bf7873063fe93'
    OR "p_ci_pipelines"."project_id" = 74823
    AND "p_ci_pipelines"."sha" = 'f0e71067189b4a400434cbc74d45587886f735b8')
ORDER BY
    "p_ci_pipelines"."sha" ASC,
    "p_ci_pipelines"."project_id" ASC,
    "p_ci_pipelines"."id" DESC

Query plan

  1. Cold run: https://console.postgres.ai/gitlab/gitlab-production-ci/sessions/33514/commands/103215
  2. Warm run: https://console.postgres.ai/gitlab/gitlab-production-ci/sessions/33514/commands/103216

Query 2

SQL

Click to expand
SELECT
    "projects"."id",
    "projects"."name",
    "projects"."path",
    "projects"."description",
    "projects"."created_at",
    "projects"."updated_at",
    "projects"."creator_id",
    "projects"."namespace_id",
    "projects"."last_activity_at",
    "projects"."import_url",
    "projects"."visibility_level",
    "projects"."archived",
    "projects"."avatar",
    "projects"."merge_requests_template",
    "projects"."star_count",
    "projects"."merge_requests_rebase_enabled",
    "projects"."import_type",
    "projects"."import_source",
    "projects"."approvals_before_merge",
    "projects"."reset_approvals_on_push",
    "projects"."merge_requests_ff_only_enabled",
    "projects"."issues_template",
    "projects"."mirror",
    "projects"."mirror_last_update_at",
    "projects"."mirror_last_successful_update_at",
    "projects"."mirror_user_id",
    "projects"."shared_runners_enabled",
    "projects"."runners_token",
    "projects"."build_allow_git_fetch",
    "projects"."build_timeout",
    "projects"."mirror_trigger_builds",
    "projects"."pending_delete",
    "projects"."public_builds",
    "projects"."last_repository_check_failed",
    "projects"."last_repository_check_at",
    "projects"."only_allow_merge_if_pipeline_succeeds",
    "projects"."has_external_issue_tracker",
    "projects"."repository_storage",
    "projects"."repository_read_only",
    "projects"."request_access_enabled",
    "projects"."has_external_wiki",
    "projects"."ci_config_path",
    "projects"."lfs_enabled",
    "projects"."description_html",
    "projects"."only_allow_merge_if_all_discussions_are_resolved",
    "projects"."repository_size_limit",
    "projects"."printing_merge_request_link_enabled",
    "projects"."auto_cancel_pending_pipelines",
    "projects"."service_desk_enabled",
    "projects"."cached_markdown_version",
    "projects"."delete_error",
    "projects"."last_repository_updated_at",
    "projects"."disable_overriding_approvers_per_merge_request",
    "projects"."storage_version",
    "projects"."resolve_outdated_diff_discussions",
    "projects"."remote_mirror_available_overridden",
    "projects"."only_mirror_protected_branches",
    "projects"."pull_mirror_available_overridden",
    "projects"."jobs_cache_index",
    "projects"."external_authorization_classification_label",
    "projects"."mirror_overwrites_diverged_branches",
    "projects"."pages_https_only",
    "projects"."external_webhook_token",
    "projects"."packages_enabled",
    "projects"."merge_requests_author_approval",
    "projects"."pool_repository_id",
    "projects"."runners_token_encrypted",
    "projects"."bfg_object_map",
    "projects"."detected_repository_languages",
    "projects"."merge_requests_disable_committers_approval",
    "projects"."require_password_to_approve",
    "projects"."max_pages_size",
    "projects"."max_artifacts_size",
    "projects"."pull_mirror_branch_prefix",
    "projects"."remove_source_branch_after_merge",
    "projects"."marked_for_deletion_at",
    "projects"."marked_for_deletion_by_user_id",
    "projects"."autoclose_referenced_issues",
    "projects"."suggestion_commit_message",
    "projects"."project_namespace_id",
    "projects"."hidden",
    "projects"."organization_id"
FROM
    "projects"
WHERE
    "projects"."id" IN (74823, 278964, 7071551, 7750063)

Query plan

https://console.postgres.ai/gitlab/gitlab-production-main/sessions/33198/commands/102524

Click to expand
 Index Scan using projects_pkey on public.projects  (cost=0.56..13.76 rows=4 width=857) (actual time=6.008..17.247 rows=4 loops=1)
   Index Cond: (projects.id = ANY ('{74823,278964,7071551,7750063}'::integer[]))
   Buffers: shared hit=12 read=14 dirtied=6
   WAL: records=2 fpi=2 bytes=14170
   I/O Timings: read=15.305 write=0.000
Settings: jit = 'off', random_page_cost = '1.5', seq_page_cost = '4', work_mem = '100MB', effective_cache_size = '472585MB'

Query 3

SQL

Click to expand
SELECT
    "project_features".*
FROM
    "project_features"
WHERE
    "project_features"."project_id" IN (74823, 278964, 7071551, 7750063)

Query plan

https://console.postgres.ai/gitlab/gitlab-production-main/sessions/33198/commands/102523

Click to expand
 Index Scan using index_project_features_on_project_id_include_container_registry on public.project_features  (cost=0.57..12.56 rows=4 width=112) (actual time=4.991..14.738 rows=4 loops=1)
   Index Cond: (project_features.project_id = ANY ('{74823,278964,7071551,7750063}'::integer[]))
   Buffers: shared hit=9 read=14
   I/O Timings: read=14.637 write=0.000
Settings: effective_cache_size = '472585MB', jit = 'off', random_page_cost = '1.5', seq_page_cost = '4', work_mem = '100MB'

Comparison to current HAML view queries

To compare just the pipeline status query I changed app/views/shared/projects/_project.html.haml to:

- avatar = local_assigns[:avatar].nil? || local_assigns[:avatar]
- stars = local_assigns[:stars].nil? || local_assigns[:stars]
- forks = local_assigns[:forks].nil? || local_assigns[:forks]
- merge_requests = local_assigns[:merge_requests].nil? || local_assigns[:merge_requests]
- issues = local_assigns[:issues].nil? || local_assigns[:issues]
- pipeline_status = local_assigns[:pipeline_status].nil? || local_assigns[:pipeline_status]
- skip_namespace = local_assigns[:skip_namespace]
- compact_mode = local_assigns[:compact_mode]
- use_creator_avatar = local_assigns[:use_creator_avatar]
- show_pipeline_status_icon = pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
- last_pipeline = last_pipeline_from_status_cache(project) if show_pipeline_status_icon
- css_controls_class = "with-pipeline-status" if show_pipeline_status_icon && last_pipeline.present?
- css_metadata_classes = "gl-flex gl-items-center !gl-text-inherit icon-wrapper has-tooltip"

%li.project-row
  .project-cell
    .gl-flex.gl-items-center.gl-mt-3
      .controls.gl-flex.gl-items-center
        - if show_pipeline_status_icon && last_pipeline.present?
          - pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref)
          %span.icon-wrapper.pipeline-status
            = render 'ci/status/icon', status: last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path

HAML version

Makes a SQL call for each project causing N+1

Click to expand
SELECT
    "ci_pipelines".*
FROM
    "ci_pipelines"
WHERE
    "ci_pipelines"."project_id" = 8
    AND ("ci_pipelines"."source" IN (1, 2, 3, 4, 5, 6, 7, 8, 10, 11)
        OR "ci_pipelines"."source" IS NULL)
    AND "ci_pipelines"."sha" = '588440f66559714280628a4f9799f0c4eb880a4a'
ORDER BY
    "ci_pipelines"."id" DESC
LIMIT 1


SELECT
    "ci_pipelines".*
FROM
    "ci_pipelines"
WHERE
    "ci_pipelines"."project_id" = 7
    AND ("ci_pipelines"."source" IN (1, 2, 3, 4, 5, 6, 7, 8, 10, 11)
        OR "ci_pipelines"."source" IS NULL)
    AND "ci_pipelines"."sha" = 'c36b5a7137a25b64c35b1d5f8bfd8a7e60581bfb'
ORDER BY
    "ci_pipelines"."id" DESC
LIMIT 1

Makes a Gitaly call even if pipelines status is cached

GraphQL version

Only makes one SQL query to get project pipelines

Click to expand
SELECT
    "ci_pipelines".*
FROM
    "ci_pipelines"
WHERE ("ci_pipelines"."project_id" = 7
    AND "ci_pipelines"."sha" = 'c36b5a7137a25b64c35b1d5f8bfd8a7e60581bfb'
    OR "ci_pipelines"."project_id" = 8
    AND "ci_pipelines"."sha" = '588440f66559714280628a4f9799f0c4eb880a4a')

Only makes a Gitaly call if pipeline status is not cached

How to set up and validate locally

  1. Open rails console (bin/rails console)
  2. Clear pipeline cache
projects = Project.all
projects.each { |project| Gitlab::Cache::Ci::ProjectPipelineStatus.new(project).delete_from_cache }
  1. Go to http://gdk.test:3000/-/graphql-explorer
  2. Run this query (make sure you have a project with a pipeline)
query getProjects {
  projects(membership: true) {
    nodes {
      latestPipelineDetailedStatus {
        name
        label
      }
    }
  }
}
  1. Copy the correlationId
  2. Go to http://gdk.test:3000/
  3. In the upper right corner of performance bar, click the + symbol and pasted the request ID
  4. Choose request from dropdown
  5. Run the query again without clearing pipeline cache to see cached result
Edited by Peter Hegman

Merge request reports

Loading