Commit 2067fc37 authored by Fabio Pitino's avatar Fabio Pitino

Merge branch 'accumulate-shared-runners-duration' into 'master'

Support tracking of shared runners duration together with CI minutes

See merge request gitlab-org/gitlab!72315
parents 81062a7a f4a5a4dc
...@@ -27,13 +27,14 @@ module Ci ...@@ -27,13 +27,14 @@ module Ci
current_month.safe_find_or_create_by(namespace_id: namespace_id) current_month.safe_find_or_create_by(namespace_id: namespace_id)
end end
def self.increase_usage(usage, amount) def self.increase_usage(usage, increments)
return unless amount > 0 increment_params = increments.select { |_attribute, value| value > 0 }
return if increment_params.empty?
# The use of `update_counters` ensures we do a SQL update rather than # The use of `update_counters` ensures we do a SQL update rather than
# incrementing the counter for the object in memory and then save it. # incrementing the counter for the object in memory and then save it.
# This is better for concurrent updates. # This is better for concurrent updates.
update_counters(usage, amount_used: amount) update_counters(usage, increment_params)
end end
def self.reset_current_usage(namespace) def self.reset_current_usage(namespace)
......
...@@ -30,13 +30,14 @@ module Ci ...@@ -30,13 +30,14 @@ module Ci
current_month.safe_find_or_create_by(project_id: project_id) current_month.safe_find_or_create_by(project_id: project_id)
end end
def self.increase_usage(usage, amount) def self.increase_usage(usage, increments)
return unless amount > 0 increment_params = increments.select { |_attribute, value| value > 0 }
return if increment_params.empty?
# The use of `update_counters` ensures we do a SQL update rather than # The use of `update_counters` ensures we do a SQL update rather than
# incrementing the counter for the object in memory and then save it. # incrementing the counter for the object in memory and then save it.
# This is better for concurrent updates. # This is better for concurrent updates.
update_counters(usage, amount_used: amount) update_counters(usage, increment_params)
end end
end end
end end
......
...@@ -16,10 +16,10 @@ module Ci ...@@ -16,10 +16,10 @@ module Ci
end end
# Updates the project and namespace usage based on the passed consumption amount # Updates the project and namespace usage based on the passed consumption amount
def execute(consumption) def execute(consumption, duration = nil)
legacy_track_usage_of_monthly_minutes(consumption) legacy_track_usage_of_monthly_minutes(consumption)
ensure_idempotency { track_usage_of_monthly_minutes(consumption) } ensure_idempotency { track_monthly_usage(consumption, duration.to_i) }
send_minutes_email_notification send_minutes_email_notification
end end
...@@ -53,14 +53,23 @@ module Ci ...@@ -53,14 +53,23 @@ module Ci
update_legacy_namespace_minutes(consumption_in_seconds) update_legacy_namespace_minutes(consumption_in_seconds)
end end
def track_usage_of_monthly_minutes(consumption) def track_monthly_usage(consumption, duration)
# preload minutes usage data outside of transaction # preload minutes usage data outside of transaction
project_usage project_usage
namespace_usage namespace_usage
::Ci::Minutes::NamespaceMonthlyUsage.transaction do ::Ci::Minutes::NamespaceMonthlyUsage.transaction do
::Ci::Minutes::NamespaceMonthlyUsage.increase_usage(namespace_usage, consumption) if namespace_usage if namespace_usage
::Ci::Minutes::ProjectMonthlyUsage.increase_usage(project_usage, consumption) if project_usage ::Ci::Minutes::NamespaceMonthlyUsage.increase_usage(namespace_usage,
amount_used: consumption,
shared_runners_duration: duration)
end
if project_usage
::Ci::Minutes::ProjectMonthlyUsage.increase_usage(project_usage,
amount_used: consumption,
shared_runners_duration: duration)
end
end end
end end
......
...@@ -13,10 +13,10 @@ module Ci ...@@ -13,10 +13,10 @@ module Ci
# used by the service object. # used by the service object.
sidekiq_options retry: 3 sidekiq_options retry: 3
def perform(consumption, project_id, namespace_id, build_id) def perform(consumption, project_id, namespace_id, build_id, params = {})
::Ci::Minutes::UpdateProjectAndNamespaceUsageService ::Ci::Minutes::UpdateProjectAndNamespaceUsageService
.new(project_id, namespace_id, build_id) .new(project_id, namespace_id, build_id)
.execute(consumption) .execute(consumption, params[:duration].to_i)
end end
end end
end end
......
...@@ -71,27 +71,7 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do ...@@ -71,27 +71,7 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do
end end
describe '.increase_usage' do describe '.increase_usage' do
subject { described_class.increase_usage(current_usage, amount) } it_behaves_like 'CI minutes increase usage'
context 'when amount is greater than 0' do
let(:amount) { 10.5 }
it 'updates the current month usage' do
subject
expect(current_usage.reload.amount_used).to eq(110.5)
end
end
context 'when amount is less or equal to 0' do
let(:amount) { -2.0 }
it 'does not update the current month usage' do
subject
expect(current_usage.reload.amount_used).to eq(100.0)
end
end
end end
describe '.for_namespace' do describe '.for_namespace' do
......
...@@ -66,29 +66,13 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do ...@@ -66,29 +66,13 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do
end end
describe '.increase_usage' do describe '.increase_usage' do
subject { described_class.increase_usage(usage, amount) } let_it_be_with_refind(:current_usage) do
create(:ci_project_monthly_usage,
let(:usage) { create(:ci_project_monthly_usage, project: project, amount_used: 100.0) } project: project,
amount_used: 100)
context 'when amount is greater than 0' do
let(:amount) { 10.5 }
it 'updates the current month usage' do
subject
expect(usage.reload.amount_used).to eq(110.5)
end
end end
context 'when amount is less or equal to 0' do it_behaves_like 'CI minutes increase usage'
let(:amount) { -2.0 }
it 'does not update the current month usage' do
subject
expect(usage.reload.amount_used).to eq(100.0)
end
end
end end
describe '.for_namespace_monthly_usage' do describe '.for_namespace_monthly_usage' do
......
...@@ -9,13 +9,14 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageService do ...@@ -9,13 +9,14 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageService do
let(:namespace) { project.namespace } let(:namespace) { project.namespace }
let(:build) { create(:ci_build) } let(:build) { create(:ci_build) }
let(:consumption_minutes) { 120 } let(:consumption_minutes) { 120 }
let(:duration) { 1_000 }
let(:consumption_seconds) { consumption_minutes * 60 } let(:consumption_seconds) { consumption_minutes * 60 }
let(:namespace_amount_used) { Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(namespace_id: namespace.id).amount_used } let(:namespace_amount_used) { Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(namespace_id: namespace.id).amount_used }
let(:project_amount_used) { Ci::Minutes::ProjectMonthlyUsage.find_or_create_current(project_id: project.id).amount_used } let(:project_amount_used) { Ci::Minutes::ProjectMonthlyUsage.find_or_create_current(project_id: project.id).amount_used }
let(:service) { described_class.new(project.id, namespace.id, build.id) } let(:service) { described_class.new(project.id, namespace.id, build.id) }
describe '#execute', :clean_gitlab_redis_shared_state do describe '#execute', :clean_gitlab_redis_shared_state do
subject { service.execute(consumption_minutes) } subject { service.execute(consumption_minutes, duration) }
shared_examples 'updates legacy consumption' do shared_examples 'updates legacy consumption' do
it 'updates legacy statistics with consumption seconds' do it 'updates legacy statistics with consumption seconds' do
...@@ -26,10 +27,20 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageService do ...@@ -26,10 +27,20 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageService do
end end
shared_examples 'updates monthly consumption' do shared_examples 'updates monthly consumption' do
it 'updates monthly usage with consumption minutes' do it 'updates monthly usage for namespace' do
current_usage = Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(namespace_id: namespace.id)
expect { subject }
.to change { current_usage.reload.amount_used }.by(consumption_minutes)
.and change { current_usage.reload.shared_runners_duration }.by(duration)
end
it 'updates monthly usage for project' do
current_usage = Ci::Minutes::ProjectMonthlyUsage.find_or_create_current(project_id: project.id)
expect { subject } expect { subject }
.to change { Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(namespace_id: namespace.id).amount_used }.by(consumption_minutes) .to change { current_usage.reload.amount_used }.by(consumption_minutes)
.and change { Ci::Minutes::ProjectMonthlyUsage.find_or_create_current(project_id: project.id).amount_used }.by(consumption_minutes) .and change { current_usage.reload.shared_runners_duration }.by(duration)
end end
end end
......
# frozen_string_literal: true
RSpec.shared_examples_for 'CI minutes increase usage' do
subject { described_class.increase_usage(current_usage, increments) }
let(:increments) { { amount_used: amount } }
context 'when amount is greater than 0' do
let(:amount) { 10.5 }
it 'updates the current month usage' do
subject
expect(current_usage.reload.amount_used).to eq(110.5)
end
end
context 'when amount is less or equal to 0' do
let(:amount) { -2.0 }
it 'does not update the current month usage' do
subject
expect(current_usage.reload.amount_used).to eq(100.0)
end
end
context 'when shared_runners_duration is incremented' do
let(:increments) { { amount_used: amount, shared_runners_duration: duration } }
let(:amount) { 10.5 }
context 'when duration is positive' do
let(:duration) { 10 }
it 'updates the duration and amount used' do
subject
expect(current_usage.reload.amount_used).to eq(110.5)
expect(current_usage.shared_runners_duration).to eq(10)
end
context 'when amount_used is zero' do
let(:amount) { 0 }
it 'updates only the duration' do
subject
expect(current_usage.reload.amount_used).to eq(100.0)
expect(current_usage.shared_runners_duration).to eq(10)
end
end
end
context 'when duration is zero' do
let(:duration) { 0 }
it 'updates only the amount used' do
subject
expect(current_usage.reload.amount_used).to eq(110.5)
expect(current_usage.shared_runners_duration).to eq(0)
end
context 'when amount_used is zero' do
let(:amount) { 0 }
it 'does not perform updates' do
subject
expect(current_usage.reload.amount_used).to eq(100.0)
expect(current_usage.shared_runners_duration).to eq(0)
end
end
end
end
end
...@@ -9,35 +9,78 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageWorker do ...@@ -9,35 +9,78 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageWorker do
let(:consumption) { 100 } let(:consumption) { 100 }
let(:consumption_seconds) { consumption * 60 } let(:consumption_seconds) { consumption * 60 }
let(:duration) { 60_000 }
let(:worker) { described_class.new } let(:worker) { described_class.new }
describe '#perform', :clean_gitlab_redis_shared_state do describe '#perform', :clean_gitlab_redis_shared_state do
subject { perform_multiple([consumption, project.id, namespace.id, build.id]) } context 'when duration param is not passed in' do
subject { perform_multiple([consumption, project.id, namespace.id, build.id]) }
context 'behaves idempotently for monthly usage update' do context 'behaves idempotently for monthly usage update' do
it 'executes UpdateProjectAndNamespaceUsageService' do it 'executes UpdateProjectAndNamespaceUsageService' do
service_instance = double service_instance = double
expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).at_least(:once).and_return(service_instance) expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).at_least(:once).and_return(service_instance)
expect(service_instance).to receive(:execute).at_least(:once).with(consumption) expect(service_instance).to receive(:execute).at_least(:once).with(consumption, 0)
subject subject
end
it 'updates monthly usage but not shared_runners_duration' do
subject
namespace_usage = Ci::Minutes::NamespaceMonthlyUsage.find_by(namespace: namespace)
expect(namespace_usage.amount_used).to eq(consumption)
expect(namespace_usage.shared_runners_duration).to eq(0)
project_usage = Ci::Minutes::ProjectMonthlyUsage.find_by(project: project)
expect(project_usage.amount_used).to eq(consumption)
expect(project_usage.shared_runners_duration).to eq(0)
end
end end
it 'updates monthly usage' do it 'does not behave idempotently for legacy statistics update' do
expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).twice.and_call_original
subject subject
expect(Ci::Minutes::NamespaceMonthlyUsage.find_by(namespace: namespace).amount_used).to eq(consumption) expect(project.statistics.reload.shared_runners_seconds).to eq(2 * consumption_seconds)
expect(Ci::Minutes::ProjectMonthlyUsage.find_by(project: project).amount_used).to eq(consumption) expect(namespace.reload.namespace_statistics.shared_runners_seconds).to eq(2 * consumption_seconds)
end end
end end
it 'does not behave idempotently for legacy statistics update' do context 'when duration param is passed in' do
expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).twice.and_call_original subject { perform_multiple([consumption, project.id, namespace.id, build.id, { duration: duration }]) }
context 'behaves idempotently for monthly usage update' do
it 'executes UpdateProjectAndNamespaceUsageService' do
service_instance = double
expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).at_least(:once).and_return(service_instance)
expect(service_instance).to receive(:execute).at_least(:once).with(consumption, duration)
subject
end
subject it 'updates monthly usage and shared_runners_duration' do
subject
expect(project.statistics.reload.shared_runners_seconds).to eq(2 * consumption_seconds) namespace_usage = Ci::Minutes::NamespaceMonthlyUsage.find_by(namespace: namespace)
expect(namespace.reload.namespace_statistics.shared_runners_seconds).to eq(2 * consumption_seconds) expect(namespace_usage.amount_used).to eq(consumption)
expect(namespace_usage.shared_runners_duration).to eq(duration)
project_usage = Ci::Minutes::ProjectMonthlyUsage.find_by(project: project)
expect(project_usage.amount_used).to eq(consumption)
expect(project_usage.shared_runners_duration).to eq(duration)
end
end
it 'does not behave idempotently for legacy statistics update' do
expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).twice.and_call_original
subject
expect(project.statistics.reload.shared_runners_seconds).to eq(2 * consumption_seconds)
expect(namespace.reload.namespace_statistics.shared_runners_seconds).to eq(2 * consumption_seconds)
end
end end
end end
end end
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