Nudge Jobs API consumers to use keyset pagination by default
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
- Enable the feature flag:
bin/rails runner '::Feature.enable(:default_to_keyset_pagination_on_first_page)'
- Get an authentication token
-
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 -
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:
- install the gem:
gem install gitlab
- in a different terminal session tail the logs:
tail -f log/development.log | grep 'Ci::Build Load'
- open an
irb
consolerequire '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
- 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.
-
I have evaluated the MR acceptance checklist for this MR.