Commit f4a5a4dc authored by Fabio Pitino's avatar Fabio Pitino

Track shared runners duration together with CI minutes

Allow service object to take in input the job duration
separately than the CI minutes consumption.
parent 1e6a860a
...@@ -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