Commit a4b5b373 authored by Mark Chao's avatar Mark Chao

Merge branch '332933-track-build-minutes-on-disabled-shared-runners' into 'master'

Track build minutes for disabled shared runners

See merge request gitlab-org/gitlab!67024
parents dc3c0e6c 710b3c9d
...@@ -16,7 +16,7 @@ module Ci ...@@ -16,7 +16,7 @@ module Ci
end end
def enabled? def enabled?
namespace_eligible? && total_minutes.nonzero? namespace_root? && total_minutes.nonzero?
end end
# Status of the monthly allowance being used. # Status of the monthly allowance being used.
...@@ -48,54 +48,53 @@ module Ci ...@@ -48,54 +48,53 @@ module Ci
enabled? && total_minutes_used >= total_minutes enabled? && total_minutes_used >= total_minutes
end end
# TODO: merge this with minutes_used_up? in def percent_total_minutes_remaining
# https://gitlab.com/gitlab-org/gitlab/-/issues/332933. return 0 if total_minutes == 0
# This method is agnostic from Project#shared_runners_enabled
def actual_minutes_used_up? 100 * total_minutes_remaining.to_i / total_minutes
limit_enabled? && total_minutes_used >= total_minutes
end end
def total_minutes def current_balance
@total_minutes ||= monthly_minutes + purchased_minutes total_minutes.to_i - total_minutes_used
end end
def total_minutes_used def display_shared_runners_data?
@total_minutes_used ||= namespace.shared_runners_seconds.to_i / 60 namespace_root? && any_project_enabled?
end end
def percent_total_minutes_remaining def display_minutes_available_data?
return 0 if total_minutes == 0 display_shared_runners_data? && total_minutes.nonzero?
end
100 * total_minutes_remaining.to_i / total_minutes def total_minutes
strong_memoize(:total_minutes) do
monthly_minutes + purchased_minutes
end
end end
def namespace_eligible? def total_minutes_used
strong_memoize(:namespace_eligible) do strong_memoize(:total_minutes_used) do
namespace.root? && namespace.any_project_with_shared_runners_enabled? namespace.shared_runners_seconds.to_i / 60
end end
end end
def current_balance def any_project_enabled?
total_minutes.to_i - total_minutes_used strong_memoize(:any_project_enabled) do
namespace.any_project_with_shared_runners_enabled?
end
end end
private private
# TODO: rename to `enabled?` attr_reader :namespace
# https://gitlab.com/gitlab-org/gitlab/-/issues/332933
def limit_enabled?
strong_memoize(:limit_enabled) do
namespace.root? && !!total_minutes.nonzero?
end
end
def minutes_limit def minutes_limit
return monthly_minutes if enabled? return _('Not supported') unless display_shared_runners_data?
if namespace_eligible? if display_minutes_available_data?
_('Unlimited') monthly_minutes
else else
_('Not supported') _('Unlimited')
end end
end end
...@@ -144,14 +143,22 @@ module Ci ...@@ -144,14 +143,22 @@ module Ci
end end
def monthly_minutes def monthly_minutes
@monthly_minutes ||= (namespace.shared_runners_minutes_limit || ::Gitlab::CurrentSettings.shared_runners_minutes).to_i strong_memoize(:monthly_minutes) do
(namespace.shared_runners_minutes_limit || ::Gitlab::CurrentSettings.shared_runners_minutes).to_i
end
end end
def purchased_minutes def purchased_minutes
@purchased_minutes ||= namespace.extra_shared_runners_minutes_limit.to_i strong_memoize(:purchased_minutes) do
namespace.extra_shared_runners_minutes_limit.to_i
end
end end
attr_reader :namespace def namespace_root?
strong_memoize(:namespace_root) do
namespace.root?
end
end
end end
end end
end end
...@@ -67,8 +67,8 @@ module EE ...@@ -67,8 +67,8 @@ module EE
end end
end end
def shared_runners_minutes_limit_enabled? def cost_factor_enabled?
project.shared_runners_minutes_limit_enabled? && runner&.cost_factor_enabled?(project) runner&.cost_factor_enabled?(project)
end end
def log_geo_deleted_event def log_geo_deleted_event
......
...@@ -28,7 +28,7 @@ module EE ...@@ -28,7 +28,7 @@ module EE
def minutes_exceeded?(project) def minutes_exceeded?(project)
::Ci::Runner.any_shared_runners_with_enabled_cost_factor?(project) && ::Ci::Runner.any_shared_runners_with_enabled_cost_factor?(project) &&
project.ci_minutes_quota.actual_minutes_used_up? project.ci_minutes_quota.minutes_used_up?
end end
end end
end end
......
...@@ -250,9 +250,9 @@ module EE ...@@ -250,9 +250,9 @@ module EE
@ci_minutes_quota ||= ::Ci::Minutes::Quota.new(self) @ci_minutes_quota ||= ::Ci::Minutes::Quota.new(self)
end end
# The same method name is used also at project and job level # The same method name is used also at project level
def shared_runners_minutes_limit_enabled? def shared_runners_minutes_limit_enabled?
ci_minutes_quota.enabled? any_project_with_shared_runners_enabled? && ci_minutes_quota.enabled?
end end
def any_project_with_shared_runners_enabled? def any_project_with_shared_runners_enabled?
......
...@@ -27,7 +27,7 @@ module Ci ...@@ -27,7 +27,7 @@ module Ci
def update_pending_builds! def update_pending_builds!
return unless ::Feature.enabled?(:ci_pending_builds_maintain_ci_minutes_data, @root_namespace, type: :development, default_enabled: :yaml) return unless ::Feature.enabled?(:ci_pending_builds_maintain_ci_minutes_data, @root_namespace, type: :development, default_enabled: :yaml)
minutes_exceeded = @root_namespace.ci_minutes_quota.actual_minutes_used_up? minutes_exceeded = @root_namespace.ci_minutes_quota.minutes_used_up?
all_namespaces = @root_namespace.self_and_descendant_ids all_namespaces = @root_namespace.self_and_descendant_ids
::Ci::PendingBuild.where(namespace: all_namespaces).update_all(minutes_exceeded: minutes_exceeded) ::Ci::PendingBuild.where(namespace: all_namespaces).update_all(minutes_exceeded: minutes_exceeded)
......
...@@ -77,8 +77,8 @@ module Ci ...@@ -77,8 +77,8 @@ module Ci
ServiceResponse.error(message: 'Feature not enabled') ServiceResponse.error(message: 'Feature not enabled')
elsif !build.running? elsif !build.running?
ServiceResponse.error(message: 'Build is not running') ServiceResponse.error(message: 'Build is not running')
elsif !build.shared_runners_minutes_limit_enabled? elsif !build.cost_factor_enabled?
ServiceResponse.error(message: 'CI minutes limit not enabled for build') ServiceResponse.error(message: 'Cost factor not enabled for build')
else else
ServiceResponse.success ServiceResponse.success
end end
......
...@@ -6,7 +6,7 @@ module Ci ...@@ -6,7 +6,7 @@ module Ci
# Calculates consumption and updates the project and namespace statistics(legacy) # Calculates consumption and updates the project and namespace statistics(legacy)
# or ProjectMonthlyUsage and NamespaceMonthlyUsage(not legacy) based on the passed build. # or ProjectMonthlyUsage and NamespaceMonthlyUsage(not legacy) based on the passed build.
def execute(build) def execute(build)
return unless build.shared_runners_minutes_limit_enabled? return unless build.cost_factor_enabled?
return unless build.complete? return unless build.complete?
return unless build.duration&.positive? return unless build.duration&.positive?
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- return unless minutes_quota.enabled? - return unless minutes_quota.enabled?
- if minutes_quota.namespace_eligible? - if minutes_quota.display_shared_runners_data?
%li %li
%span.light= _('Additional minutes:') %span.light= _('Additional minutes:')
%strong %strong
......
- namespace = local_assigns.fetch(:namespace) - namespace = local_assigns.fetch(:namespace)
- minutes_quota = namespace.ci_minutes_quota - minutes_quota = namespace.ci_minutes_quota
- if minutes_quota.namespace_eligible? - if minutes_quota.display_shared_runners_data?
%li %li
%span.light= _('Pipeline minutes quota:') %span.light= _('Pipeline minutes quota:')
%strong %strong
......
- return unless Gitlab.com? - return unless Gitlab.com?
- minutes_quota = namespace.ci_minutes_quota - minutes_quota = namespace.ci_minutes_quota
- return unless minutes_quota.enabled? && minutes_quota.purchased_minutes_report.limit > 0 - return unless minutes_quota.display_minutes_available_data? && minutes_quota.purchased_minutes_report.limit > 0
.row .row
.col-sm-6 .col-sm-6
......
...@@ -26,9 +26,9 @@ ...@@ -26,9 +26,9 @@
= link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'shared-runners-pipeline-minutes-quota'), target: '_blank', 'aria-label': _('Shared runners help link') = link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'shared-runners-pipeline-minutes-quota'), target: '_blank', 'aria-label': _('Shared runners help link')
.col-sm-6.right .col-sm-6.right
- if minutes_quota.enabled? - if minutes_quota.display_minutes_available_data?
#{minutes_quota.monthly_percent_used}% used #{minutes_quota.monthly_percent_used}% used
- elsif !namespace.any_project_with_shared_runners_enabled? - elsif !minutes_quota.any_project_enabled?
0% used 0% used
- else - else
= s_('UsageQuota|Unlimited') = s_('UsageQuota|Unlimited')
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
= _('Minutes') = _('Minutes')
%tbody %tbody
- if !namespace.any_project_with_shared_runners_enabled? - if !minutes_quota.any_project_enabled?
%tr %tr
%td{ colspan: 2 } %td{ colspan: 2 }
.nothing-here-block .nothing-here-block
......
...@@ -63,6 +63,7 @@ RSpec.describe EE::NamespacesHelper do ...@@ -63,6 +63,7 @@ RSpec.describe EE::NamespacesHelper do
context 'and the namespace is eligible for unlimited' do context 'and the namespace is eligible for unlimited' do
before do before do
allow(quota).to receive(:namespace_eligible?).and_return(true) allow(quota).to receive(:namespace_eligible?).and_return(true)
allow(user_group).to receive(:any_project_with_shared_runners_enabled?).and_return(true)
end end
it 'returns Unlimited for the limit section' do it 'returns Unlimited for the limit section' do
......
...@@ -48,48 +48,28 @@ RSpec.describe Ci::Build do ...@@ -48,48 +48,28 @@ RSpec.describe Ci::Build do
it { is_expected.to have_one(:dast_scanner_profile).class_name('DastScannerProfile').through(:dast_scanner_profiles_build) } it { is_expected.to have_one(:dast_scanner_profile).class_name('DastScannerProfile').through(:dast_scanner_profiles_build) }
end end
describe '#shared_runners_minutes_limit_enabled?' do describe '#cost_factor_enabled?' do
subject { job.shared_runners_minutes_limit_enabled? } subject { job.cost_factor_enabled? }
shared_examples 'depends on runner presence and type' do context 'for shared runner' do
context 'for shared runner' do before do
before do job.runner = create(:ci_runner, :instance)
job.runner = create(:ci_runner, :instance)
end
context 'when project#shared_runners_minutes_limit_enabled? is true' do
specify do
expect(job.project).to receive(:shared_runners_minutes_limit_enabled?)
.and_return(true)
is_expected.to be_truthy
end
end
context 'when project#shared_runners_minutes_limit_enabled? is false' do
specify do
expect(job.project).to receive(:shared_runners_minutes_limit_enabled?)
.and_return(false)
is_expected.to be_falsey
end
end
end end
context 'with specific runner' do it { is_expected.to be_truthy }
before do end
job.runner = create(:ci_runner, :project)
end
it { is_expected.to be_falsey } context 'with specific runner' do
before do
job.runner = create(:ci_runner, :project)
end end
context 'without runner' do it { is_expected.to be_falsey }
it { is_expected.to be_falsey }
end
end end
it_behaves_like 'depends on runner presence and type' context 'without runner' do
it { is_expected.to be_falsey }
end
end end
context 'updates pipeline minutes' do context 'updates pipeline minutes' do
......
...@@ -5,7 +5,7 @@ RSpec.describe Ci::Minutes::Quota do ...@@ -5,7 +5,7 @@ RSpec.describe Ci::Minutes::Quota do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:namespace) do let_it_be_with_reload(:namespace) do
create(:namespace, namespace_statistics: create(:namespace_statistics)) create(:group, namespace_statistics: create(:namespace_statistics))
end end
let(:quota) { described_class.new(namespace) } let(:quota) { described_class.new(namespace) }
...@@ -38,13 +38,13 @@ RSpec.describe Ci::Minutes::Quota do ...@@ -38,13 +38,13 @@ RSpec.describe Ci::Minutes::Quota do
end end
end end
context 'when namespace does not have projects with shared runners enabled' do context 'when namespace has a limit but does not have projects with shared runners enabled' do
before do before do
project.update!(shared_runners_enabled: false) project.update!(shared_runners_enabled: false)
allow(namespace).to receive(:shared_runners_minutes_limit).and_return(1000) allow(namespace).to receive(:shared_runners_minutes_limit).and_return(1000)
end end
it { is_expected.to be_falsey } it { is_expected.to be_truthy }
end end
end end
...@@ -67,7 +67,8 @@ RSpec.describe Ci::Minutes::Quota do ...@@ -67,7 +67,8 @@ RSpec.describe Ci::Minutes::Quota do
context 'when the quota is not enabled' do context 'when the quota is not enabled' do
before do before do
allow(quota).to receive(:enabled?).and_return(false) allow(quota).to receive(:enabled?).and_return(false)
allow(quota).to receive(:namespace_eligible?).and_return(namespace_eligible) allow(namespace).to receive(:root?).and_return(namespace_eligible)
allow(namespace).to receive(:any_project_with_shared_runners_enabled?).and_return(true)
end end
context 'when the namespace is not eligible' do context 'when the namespace is not eligible' do
...@@ -114,6 +115,7 @@ RSpec.describe Ci::Minutes::Quota do ...@@ -114,6 +115,7 @@ RSpec.describe Ci::Minutes::Quota do
context 'when limited' do context 'when limited' do
before do before do
allow(quota).to receive(:enabled?).and_return(true) allow(quota).to receive(:enabled?).and_return(true)
allow(namespace).to receive(:any_project_with_shared_runners_enabled?).and_return(true)
namespace.shared_runners_minutes_limit = 100 namespace.shared_runners_minutes_limit = 100
end end
...@@ -257,6 +259,7 @@ RSpec.describe Ci::Minutes::Quota do ...@@ -257,6 +259,7 @@ RSpec.describe Ci::Minutes::Quota do
with_them do with_them do
before do before do
allow(quota).to receive(:enabled?).and_return(limit_enabled) allow(quota).to receive(:enabled?).and_return(limit_enabled)
allow(namespace).to receive(:any_project_with_shared_runners_enabled?).and_return(true)
namespace.shared_runners_minutes_limit = monthly_limit namespace.shared_runners_minutes_limit = monthly_limit
namespace.extra_shared_runners_minutes_limit = purchased_limit namespace.extra_shared_runners_minutes_limit = purchased_limit
namespace.namespace_statistics.shared_runners_seconds = minutes_used.minutes namespace.namespace_statistics.shared_runners_seconds = minutes_used.minutes
...@@ -326,25 +329,6 @@ RSpec.describe Ci::Minutes::Quota do ...@@ -326,25 +329,6 @@ RSpec.describe Ci::Minutes::Quota do
end end
end end
describe '#actual_minutes_used_up?' do
subject { quota.actual_minutes_used_up? }
where(:minutes_used, :minutes_limit, :result, :title) do
100 | 0 | false | 'limit not enabled'
99 | 100 | false | 'total minutes not used'
101 | 100 | true | 'total minutes used'
end
with_them do
before do
allow(namespace).to receive(:shared_runners_seconds).and_return(minutes_used.minutes)
namespace.shared_runners_minutes_limit = minutes_limit
end
it { is_expected.to eq(result) }
end
end
describe '#total_minutes' do describe '#total_minutes' do
subject { quota.total_minutes } subject { quota.total_minutes }
...@@ -410,49 +394,101 @@ RSpec.describe Ci::Minutes::Quota do ...@@ -410,49 +394,101 @@ RSpec.describe Ci::Minutes::Quota do
end end
end end
describe '#namespace_eligible?' do describe '#any_project_enabled?' do
subject { quota.namespace_eligible? } let_it_be(:project) { create(:project, namespace: namespace) }
context 'when namespace is a subgroup' do context 'when namespace has any project with shared runners enabled' do
it 'is false' do before do
allow(namespace).to receive(:root?).and_return(false) project.update!(shared_runners_enabled: true)
end
expect(subject).to be_falsey it 'returns true' do
expect(quota.any_project_enabled?).to be_truthy
end end
end end
context 'when namespace is root' do context 'when namespace has no projects with shared runners enabled' do
before do before do
create(:project, namespace: namespace, shared_runners_enabled: shared_runners_enabled) project.update!(shared_runners_enabled: false)
end end
context 'and it has a project without any shared runner enabled' do it 'returns false' do
let(:shared_runners_enabled) { false } expect(quota.any_project_enabled?).to be_falsey
end
end
it 'is false' do it 'does not trigger additional queries when called multiple times' do
expect(subject).to be_falsey # memoizes the result
end quota.any_project_enabled?
# count
actual = ActiveRecord::QueryRecorder.new do
quota.any_project_enabled?
end
expect(actual.count).to eq(0)
end
end
describe '#display_shared_runners_data?' do
let_it_be(:project) { create(:project, namespace: namespace, shared_runners_enabled: true) }
subject { quota.display_shared_runners_data? }
context 'when the namespace is root and it has a project with shared runners enabled' do
it { is_expected.to be_truthy }
end
context 'when the namespace is not root' do
let(:namespace) { create(:group, :nested) }
it { is_expected.to be_falsey }
end
context 'when the namespaces has no project with shared runners enabled' do
before do
project.update!(shared_runners_enabled: false)
end end
context 'and it has a project with shared runner enabled' do it { is_expected.to be_falsey }
let(:shared_runners_enabled) { true } end
end
describe '#display_minutes_available_data?' do
let_it_be(:project) { create(:project, namespace: namespace, shared_runners_enabled: true) }
it 'is true' do subject { quota.display_minutes_available_data? }
expect(subject).to be_truthy
context 'when the namespace is root and it has a project with shared runners enabled' do
context 'when there is a minutes limit' do
before do
namespace.update!(shared_runners_minutes_limit: 200)
end end
it { is_expected.to be_truthy }
end
context 'when there is no minutes limit' do
before do
namespace.update!(shared_runners_minutes_limit: 0)
end
it { is_expected.to be_falsey }
end end
end end
it 'does not trigger N+1 queries when called multiple times' do context 'when the namespace is not root' do
# memoizes the result let(:namespace) { create(:group, :nested) }
quota.namespace_eligible?
# count it { is_expected.to be_falsey }
actual = ActiveRecord::QueryRecorder.new do end
quota.namespace_eligible?
context 'when the namespaces has no project with shared runners enabled' do
before do
project.update!(shared_runners_enabled: false)
end end
expect(actual.count).to eq(0) it { is_expected.to be_falsey }
end end
end end
end end
...@@ -50,7 +50,7 @@ RSpec.describe Ci::PendingBuild do ...@@ -50,7 +50,7 @@ RSpec.describe Ci::PendingBuild do
context 'when ci minutes are not available' do context 'when ci minutes are not available' do
before do before do
allow_next_instance_of(::Ci::Minutes::Quota) do |instance| allow_next_instance_of(::Ci::Minutes::Quota) do |instance|
allow(instance).to receive(:actual_minutes_used_up?).and_return(true) allow(instance).to receive(:minutes_used_up?).and_return(true)
end end
end end
......
...@@ -30,7 +30,7 @@ RSpec.describe Ci::Minutes::RefreshCachedDataService do ...@@ -30,7 +30,7 @@ RSpec.describe Ci::Minutes::RefreshCachedDataService do
context 'when user purchases more ci minutes for a given namespace' do context 'when user purchases more ci minutes for a given namespace' do
before do before do
allow_next_instance_of(::Ci::Minutes::Quota) do |instance| allow_next_instance_of(::Ci::Minutes::Quota) do |instance|
allow(instance).to receive(:actual_minutes_used_up?).and_return(false) allow(instance).to receive(:minutes_used_up?).and_return(false)
end end
end end
......
...@@ -69,15 +69,15 @@ RSpec.describe Ci::Minutes::TrackLiveConsumptionService do ...@@ -69,15 +69,15 @@ RSpec.describe Ci::Minutes::TrackLiveConsumptionService do
context 'when runner is not of instance type' do context 'when runner is not of instance type' do
let(:runner) { create(:ci_runner, :project) } let(:runner) { create(:ci_runner, :project) }
it_behaves_like 'returns early', 'CI minutes limit not enabled for build' it_behaves_like 'returns early', 'Cost factor not enabled for build'
end end
context 'when shared runners limit is not enabled for build' do context 'when cost factor is not enabled for build' do
before do before do
allow(build).to receive(:shared_runners_minutes_limit_enabled?).and_return(false) allow(build).to receive(:cost_factor_enabled?).and_return(false)
end end
it_behaves_like 'returns early', 'CI minutes limit not enabled for build' it_behaves_like 'returns early', 'Cost factor not enabled for build'
end end
context 'when build has not been tracked recently' do context 'when build has not been tracked recently' do
......
...@@ -202,7 +202,7 @@ RSpec.describe PipelineSerializer do ...@@ -202,7 +202,7 @@ RSpec.describe PipelineSerializer do
# Existing numbers are high and require performance optimization # Existing numbers are high and require performance optimization
# Ongoing issue: # Ongoing issue:
# https://gitlab.com/gitlab-org/gitlab/-/issues/225156 # https://gitlab.com/gitlab-org/gitlab/-/issues/225156
expected_queries = Gitlab.ee? ? 77 : 70 expected_queries = Gitlab.ee? ? 74 : 70
expect(recorded.count).to be_within(2).of(expected_queries) expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0) expect(recorded.cached_count).to eq(0)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment