Skip to content

Nudge Jobs API consumers to use keyset pagination by default

Marius Bobin requested to merge mb-default-keyset-pagination into master

What does this MR do and why?

Some of the API clients use the pagination headers to fetch the next page. If we default to keyset pagination on the first page, we can force the clients to use it without requiring them to add the pagination param. This makes the keyset pagination opt-out.

For example, the gitlab API client uses the Link header to fetch the next page: https://github.com/NARKOZ/gitlab/blob/master/lib/gitlab/paginated_response.rb

Reasoning:

The API clients that don't use the pagination link header more than likely use a for/loop to go through the pages and that always start with the page=1 parameter. This change will not affect them, but we control the users that follow the headers and enable the optimal pagination for them without requiring action on their part.

This can be considered breaking change because we don't including the 'X-Per-Page', 'X-Page', 'X-Next-Page', 'X-Prev-Page' headers with keyset pagination.

Related to: https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/24660

Sample

# without page we get the keyset header
> curl -I -H 'PRIVATE-TOKEN: glpat-PAT' 'http://gdev.bobin.ro:3000/api/v4/projects/<project_id>/jobs' 2>/dev/null | grep Link
Link: <http://gdev.bobin.ro:3000/api/v4/projects/265/jobs?cursor=eyJpZCI6IjE0OTc3MSIsIl9rZCI6Im4ifQ%3D%3D&id=265&page=1&per_page=20>; rel="next"
# with page we get the offset header
> curl -I -H 'PRIVATE-TOKEN: glpat-PAT' 'http://gdev.bobin.ro:3000/api/v4/projects/<project_id>/jobs?page=1' 2>/dev/null | grep Link
Link: <http://gdev.bobin.ro:3000/api/v4/projects/265/jobs?id=265&page=2&per_page=20>; rel="next", <http://gdev.bobin.ro:3000/api/v4/projects/265/jobs?id=265&page=1&per_page=20>; rel="first" 

How to set up and validate locally

  1. Enable the feature flag: bin/rails runner '::Feature.enable(:default_to_keyset_pagination_on_first_page)'
  2. Get an authentication token
  3. curl -I -H 'PRIVATE-TOKEN: glpat-XXXXXXXXXXXXX' 'http://localhost:3000/api/v4/projects/265/jobs' 2>/dev/null | grep Link to get the keyset pagination header
  4. curl -I -H 'PRIVATE-TOKEN: glpat-XXXXXXXXXXXXX' 'http://localhost:3000/api/v4/projects/265/jobs?page=1' 2>/dev/null | grep Link to get the offset pagination

Test using the gitlab api client:

  1. install the gem: gem install gitlab
  2. in a different terminal session tail the logs: tail -f log/development.log | grep 'Ci::Build Load'
  3. open an irb console
    • require 'gitlab'
    • client = Gitlab.client(endpoint: 'http://localhost:3000/api/v4', private_token: 'glpat-PAT')
    • client.jobs(@project_id).auto_paginate - project with more than 20 jobs
  4. in the logs terminal the queries should look like this:
  Ci::Build Load (1.0ms)  SELECT "p_ci_builds".* FROM "p_ci_builds" WHERE "p_ci_builds"."type" = 'Ci::Build' AND "p_ci_builds"."project_id" = 265 AND ("p_ci_builds"."id" < 137331) ORDER BY "p_ci_builds"."id" DESC LIMIT 21 /*application:web,correlation_id:01HDNXQYXPK517DKCRP36FX5Y6,endpoint_id:GET /api/:version/projects/:id/jobs,db_config_name:ci,line:/lib/gitlab/pagination/keyset/paginator.rb:55:in `records'*/
  Ci::Build Load (0.6ms)  SELECT "p_ci_builds".* FROM "p_ci_builds" WHERE "p_ci_builds"."type" = 'Ci::Build' AND "p_ci_builds"."project_id" = 265 AND ("p_ci_builds"."id" < 137311) ORDER BY "p_ci_builds"."id" DESC LIMIT 21 /*application:web,correlation_id:01HDNXQZ7MHH2MKM3D3DPEEQXD,endpoint_id:GET /api/:version/projects/:id/jobs,db_config_name:ci,line:/lib/gitlab/pagination/keyset/paginator.rb:55:in `records'*/
  Ci::Build Load (0.7ms)  SELECT "p_ci_builds".* FROM "p_ci_builds" WHERE "p_ci_builds"."type" = 'Ci::Build' AND "p_ci_builds"."project_id" = 265 AND ("p_ci_builds"."id" < 137291) ORDER BY "p_ci_builds"."id" DESC LIMIT 21 /*application:web,correlation_id:01HDNXQZH9PARE8ZFNE6H7TM8Y,endpoint_id:GET /api/:version/projects/:id/jobs,db_config_name:ci,line:/lib/gitlab/pagination/keyset/paginator.rb:55:in `records'*/
  Ci::Build Load (0.9ms)  SELECT "p_ci_builds".* FROM "p_ci_builds" WHERE "p_ci_builds"."type" = 'Ci::Build' AND "p_ci_builds"."project_id" = 265 AND ("p_ci_builds"."id" < 137271) ORDER BY "p_ci_builds"."id" DESC LIMIT 21 /*application:web,correlation_id:01HDNXQZTYF1T4TPCR5V15GWES,endpoint_id:GET /api/:version/projects/:id/jobs,db_config_name:ci,line:/lib/gitlab/pagination/keyset/paginator.rb:55:in `records'*/
  Ci::Build Load (0.6ms)  SELECT "p_ci_builds".* FROM "p_ci_builds" WHERE "p_ci_builds"."type" = 'Ci::Build' AND "p_ci_builds"."project_id" = 265 AND ("p_ci_builds"."id" < 137251) ORDER BY "p_ci_builds"."id" DESC LIMIT 21 /*application:web,correlation_id:01HDNXR04BYXW1VY1Q4CWNEKXX,endpoint_id:GET /api/:version/projects/:id/jobs,db_config_name:ci,line:/lib/gitlab/pagination/keyset/paginator.rb:55:in `records'*/
  Ci::Build Load (0.9ms)  SELECT "p_ci_builds".* FROM "p_ci_builds" WHERE "p_ci_builds"."type" = 'Ci::Build' AND "p_ci_builds"."project_id" = 265 AND ("p_ci_builds"."id" < 137231) ORDER BY "p_ci_builds"."id" DESC LIMIT 21 /*application:web,correlation_id:01HDNXR0E71BMB093039WFYJ2A,endpoint_id:GET /api/:version/projects/:id/jobs,db_config_name:ci,line:/lib/gitlab/pagination/keyset/paginator.rb:55:in `records'*/
  Ci::Build Load (0.6ms)  SELECT "p_ci_builds".* FROM "p_ci_builds" WHERE "p_ci_builds"."type" = 'Ci::Build' AND "p_ci_builds"."project_id" = 265 AND ("p_ci_builds"."id" < 137211) ORDER BY "p_ci_builds"."id" DESC LIMIT 21 /*application:web,correlation_id:01HDNXR0Q2J1K8XMAQESBEQJPK,endpoint_id:GET /api/:version/projects/:id/jobs,db_config_name:ci,line:/lib/gitlab/pagination/keyset/paginator.rb:55:in `records'*/

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 Marius Bobin

Merge request reports