From dd40cb9da2167108e429e92b89223415eee1b1a8 Mon Sep 17 00:00:00 2001 From: Philipp Thun Date: Thu, 5 Mar 2026 16:13:44 +0100 Subject: [PATCH] Reduce db calls when presenting droplets Eager load metadata, buildpack_lifecycle_data, buildpack_lifecycle_buildpacks, and cnb_lifecycle_data. --- app/controllers/v3/droplets_controller.rb | 8 ++++---- app/fetchers/droplet_list_fetcher.rb | 22 ++++++++++++---------- app/presenters/v3/droplet_presenter.rb | 7 +++++++ spec/request/droplets_spec.rb | 18 ++++++++++++++++++ 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/app/controllers/v3/droplets_controller.rb b/app/controllers/v3/droplets_controller.rb index b08e10d338..1d98ec0adf 100644 --- a/app/controllers/v3/droplets_controller.rb +++ b/app/controllers/v3/droplets_controller.rb @@ -22,16 +22,16 @@ def index invalid_param!(message.errors.full_messages) unless message.valid? if app_nested? - app, dataset = DropletListFetcher.fetch_for_app(message) + app, dataset = DropletListFetcher.fetch_for_app(message, eager_loaded_associations: Presenters::V3::DropletPresenter.associated_resources) app_not_found! unless app && permission_queryer.can_read_from_space?(app.space.id, app.space.organization_id) elsif package_nested? - package, dataset = DropletListFetcher.fetch_for_package(message) + package, dataset = DropletListFetcher.fetch_for_package(message, eager_loaded_associations: Presenters::V3::DropletPresenter.associated_resources) package_not_found! unless package && permission_queryer.can_read_from_space?(package.space.id, package.space.organization_id) else dataset = if permission_queryer.can_read_globally? - DropletListFetcher.fetch_all(message) + DropletListFetcher.fetch_all(message, eager_loaded_associations: Presenters::V3::DropletPresenter.associated_resources) else - DropletListFetcher.fetch_for_spaces(message, permission_queryer.readable_space_guids) + DropletListFetcher.fetch_for_spaces(message, permission_queryer.readable_space_guids, eager_loaded_associations: Presenters::V3::DropletPresenter.associated_resources) end end diff --git a/app/fetchers/droplet_list_fetcher.rb b/app/fetchers/droplet_list_fetcher.rb index b85721c627..9ff62bbbff 100644 --- a/app/fetchers/droplet_list_fetcher.rb +++ b/app/fetchers/droplet_list_fetcher.rb @@ -3,32 +3,34 @@ module VCAP::CloudController class DropletListFetcher < BaseListFetcher class << self - def fetch_all(message) - dataset = DropletModel.dataset - filter(message, nil, nil, dataset) + def fetch_all(message, eager_loaded_associations: []) + filter(message, nil, nil, droplet_dataset(eager_loaded_associations)) end - def fetch_for_spaces(message, space_guids) - dataset = DropletModel.dataset - filter(message, nil, space_guids, dataset) + def fetch_for_spaces(message, space_guids, eager_loaded_associations: []) + filter(message, nil, space_guids, droplet_dataset(eager_loaded_associations)) end - def fetch_for_app(message) + def fetch_for_app(message, eager_loaded_associations: []) app = AppModel.where(guid: message.app_guid).first return nil unless app - [app, filter(message, app, nil, app.droplets_dataset)] + [app, filter(message, app, nil, droplet_dataset(eager_loaded_associations, app.droplets_dataset))] end - def fetch_for_package(message) + def fetch_for_package(message, eager_loaded_associations: []) package = PackageModel.where(guid: message.package_guid).first return nil unless package - [package, filter(message, nil, nil, package.droplets_dataset)] + [package, filter(message, nil, nil, droplet_dataset(eager_loaded_associations, package.droplets_dataset))] end private + def droplet_dataset(eager_loaded_associations, dataset=DropletModel.dataset) + dataset.eager(eager_loaded_associations) + end + def filter(message, app, space_guids, dataset) if message.requested?(:current) && app dataset = dataset.extension(:null_dataset) diff --git a/app/presenters/v3/droplet_presenter.rb b/app/presenters/v3/droplet_presenter.rb index 177eee8d2d..dfe1051f27 100644 --- a/app/presenters/v3/droplet_presenter.rb +++ b/app/presenters/v3/droplet_presenter.rb @@ -7,6 +7,13 @@ module V3 class DropletPresenter < BasePresenter include VCAP::CloudController::Presenters::Mixins::MetadataPresentationHelpers + class << self + # :labels and :annotations come from MetadataPresentationHelpers + def associated_resources + super + [{ buildpack_lifecycle_data: :buildpack_lifecycle_buildpacks }, :cnb_lifecycle_data] + end + end + def to_hash { guid: droplet.guid, diff --git a/spec/request/droplets_spec.rb b/spec/request/droplets_spec.rb index 0394021780..fb60d1d9df 100644 --- a/spec/request/droplets_spec.rb +++ b/spec/request/droplets_spec.rb @@ -607,6 +607,24 @@ droplet2.buildpack_lifecycle_data.update(buildpacks: ['http://buildpack.git.url.com'], stack: 'stack-2') end + context 'eager loading' do + let!(:docker_droplet_1) { VCAP::CloudController::DropletModel.make(:docker, app_guid: app_model.guid) } + let!(:docker_droplet_2) { VCAP::CloudController::DropletModel.make(:docker, app_guid: app_model.guid) } + let(:get_droplets) { -> { get '/v3/droplets', nil, developer_headers } } + + it 'eager loads associated data needed to present droplets' do + expect { get_droplets.call }.to have_queried_db_times(/SELECT .* FROM .droplets. /i, 1) + expect(last_response.status).to eq(200) + expect(parsed_response['resources'].count).to eq(4) + + expect { get_droplets.call }.to have_queried_db_times(/SELECT .* FROM .droplet_annotations. /i, 1) # instead of 4 w/o eager loading + expect { get_droplets.call }.to have_queried_db_times(/SELECT .* FROM .droplet_labels. /i, 1) # instead of 4 w/o eager loading + expect { get_droplets.call }.to have_queried_db_times(/SELECT .* FROM .buildpack_lifecycle_data. /i, 1) # instead of 4 w/o eager loading + expect { get_droplets.call }.to have_queried_db_times(/SELECT .* FROM .buildpack_lifecycle_buildpacks. /i, 1) # instead of 2 w/o eager loading + expect { get_droplets.call }.to have_queried_db_times(/SELECT .* FROM .cnb_lifecycle_data. /i, 1) # instead of 2 w/o eager loading + end + end + it_behaves_like 'list query endpoint' do let(:request) { 'v3/droplets' } let(:message) { VCAP::CloudController::DropletsListMessage }