Commit dd36b5f5 authored by Allison Browne's avatar Allison Browne Committed by Fabio Pitino

Move Minutes Usage Update to Async Job

parent fe91c0a6
...@@ -7,7 +7,7 @@ module Ci ...@@ -7,7 +7,7 @@ module Ci
Ci::ExpirePipelineCacheService.new.execute(pipeline, delete: true) Ci::ExpirePipelineCacheService.new.execute(pipeline, delete: true)
pipeline.cancel_running if pipeline.cancelable? && ::Feature.enabled?(:cancel_pipelines_prior_to_destroy, default_enabled: :yaml) pipeline.cancel_running if pipeline.cancelable? && ::Feature.enabled?(:cancel_pipelines_prior_to_destroy, pipeline.project, default_enabled: :yaml)
pipeline.reset.destroy! pipeline.reset.destroy!
......
...@@ -22,8 +22,8 @@ module Ci ...@@ -22,8 +22,8 @@ module Ci
# #
# Here we will also do any recalculation of additional minutes based on the # Here we will also do any recalculation of additional minutes based on the
# previous month usage. # previous month usage.
def self.find_or_create_current(namespace) def self.find_or_create_current(namespace_id:)
current_month.safe_find_or_create_by(namespace: namespace) current_month.safe_find_or_create_by(namespace_id: namespace_id)
end end
def self.increase_usage(usage, amount) def self.increase_usage(usage, amount)
......
...@@ -19,8 +19,8 @@ module Ci ...@@ -19,8 +19,8 @@ module Ci
# since this will lazily create an entry if it doesn't exist. # since this will lazily create an entry if it doesn't exist.
# For example, on the 1st of each month, when we update the usage for a project, # For example, on the 1st of each month, when we update the usage for a project,
# we will automatically generate new records and reset usage for the current month. # we will automatically generate new records and reset usage for the current month.
def self.find_or_create_current(project) def self.find_or_create_current(project_id:)
current_month.safe_find_or_create_by(project: project) current_month.safe_find_or_create_by(project_id: project_id)
end end
def self.increase_usage(usage, amount) def self.increase_usage(usage, amount)
......
...@@ -52,9 +52,7 @@ module EE ...@@ -52,9 +52,7 @@ module EE
state_machine :status do state_machine :status do
after_transition any => [:success, :failed, :canceled] do |build| after_transition any => [:success, :failed, :canceled] do |build|
build.run_after_commit do build.run_after_commit do
# TODO(Issue #331891): before enabling this feature flag. Move update consumption to async while keeping consumption calculation sync. if ::Feature.enabled?(:cancel_pipelines_prior_to_destroy, build.project, default_enabled: :yaml)
# This will ensure consumption is calculated before related records are deleted.
if ::Feature.enabled?(:cancel_pipelines_prior_to_destroy, default_enabled: :yaml)
::Ci::Minutes::UpdateBuildMinutesService.new(build.project, nil).execute(build) ::Ci::Minutes::UpdateBuildMinutesService.new(build.project, nil).execute(build)
end end
end end
......
...@@ -14,14 +14,20 @@ module Ci ...@@ -14,14 +14,20 @@ module Ci
return unless consumption > 0 return unless consumption > 0
# TODO(Issue #335338): Introduce async worker UpdateProjectAndNamespaceUsageWorker update_minutes(consumption)
Ci::Minutes::UpdateProjectAndNamespaceUsageService.new(project, namespace).execute(consumption)
compare_with_live_consumption(build, consumption) compare_with_live_consumption(build, consumption)
end end
private private
def update_minutes(consumption)
if ::Feature.enabled?(:cancel_pipelines_prior_to_destroy, project, default_enabled: :yaml)
::Ci::Minutes::UpdateProjectAndNamespaceUsageWorker.perform_async(consumption, project.id, namespace.id)
else
::Ci::Minutes::UpdateProjectAndNamespaceUsageService.new(project.id, namespace.id).execute(consumption)
end
end
def compare_with_live_consumption(build, consumption) def compare_with_live_consumption(build, consumption)
live_consumption = ::Ci::Minutes::TrackLiveConsumptionService.new(build).live_consumption live_consumption = ::Ci::Minutes::TrackLiveConsumptionService.new(build).live_consumption
return if live_consumption == 0 return if live_consumption == 0
......
...@@ -3,53 +3,83 @@ ...@@ -3,53 +3,83 @@
module Ci module Ci
module Minutes module Minutes
class UpdateProjectAndNamespaceUsageService class UpdateProjectAndNamespaceUsageService
def initialize(project, namespace) include Gitlab::Utils::StrongMemoize
@project = project
@namespace = namespace def initialize(project_id, namespace_id)
@project_id = project_id
@namespace_id = namespace_id
# TODO(issue 335885): Use project_id only and don't query for projects which may be deleted
@project = Project.find_by_id(project_id)
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)
consumption_in_seconds = consumption.minutes.to_i legacy_track_usage_of_monthly_minutes(consumption)
legacy_track_usage_of_monthly_minutes(consumption_in_seconds) ApplicationRecord.transaction do
track_usage_of_monthly_minutes(consumption)
track_usage_of_monthly_minutes(consumption) send_minutes_email_notification
send_minutes_email_notification end
end end
private private
def send_minutes_email_notification def send_minutes_email_notification
# `perform reset` on `project` because `Namespace#namespace_statistics` will otherwise return stale data. # `perform reset` on `project` because `Namespace#namespace_statistics` will otherwise return stale data.
# TODO(issue 335885): Remove @project
::Ci::Minutes::EmailNotificationService.new(@project.reset).execute if ::Gitlab.com? ::Ci::Minutes::EmailNotificationService.new(@project.reset).execute if ::Gitlab.com?
end end
def legacy_track_usage_of_monthly_minutes(consumption_in_seconds) def legacy_track_usage_of_monthly_minutes(consumption)
ProjectStatistics.update_counters(project_statistics, consumption_in_seconds = consumption.minutes.to_i
shared_runners_seconds: consumption_in_seconds)
NamespaceStatistics.update_counters(namespace_statistics, update_legacy_project_minutes(consumption_in_seconds)
shared_runners_seconds: consumption_in_seconds) update_legacy_namespace_minutes(consumption_in_seconds)
end end
def track_usage_of_monthly_minutes(consumption) def track_usage_of_monthly_minutes(consumption)
# TODO(issue 335885): Remove @project
return unless Feature.enabled?(:ci_minutes_monthly_tracking, @project, default_enabled: :yaml) return unless Feature.enabled?(:ci_minutes_monthly_tracking, @project, default_enabled: :yaml)
namespace_usage = ::Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(@namespace) ::Ci::Minutes::NamespaceMonthlyUsage.increase_usage(namespace_usage, consumption) if namespace_usage
project_usage = ::Ci::Minutes::ProjectMonthlyUsage.find_or_create_current(@project) ::Ci::Minutes::ProjectMonthlyUsage.increase_usage(project_usage, consumption) if project_usage
end
ApplicationRecord.transaction do def update_legacy_project_minutes(consumption_in_seconds)
::Ci::Minutes::NamespaceMonthlyUsage.increase_usage(namespace_usage, consumption) if project_statistics
::Ci::Minutes::ProjectMonthlyUsage.increase_usage(project_usage, consumption) ProjectStatistics.update_counters(project_statistics, shared_runners_seconds: consumption_in_seconds)
end
end
def update_legacy_namespace_minutes(consumption_in_seconds)
if namespace_statistics
NamespaceStatistics.update_counters(namespace_statistics, shared_runners_seconds: consumption_in_seconds)
end
end
def namespace_usage
::Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(namespace_id: @namespace_id)
end
def project_usage
strong_memoize(:project_usage) do
::Ci::Minutes::ProjectMonthlyUsage.find_or_create_current(project_id: @project_id)
rescue ActiveRecord::InvalidForeignKey
end end
end end
def namespace_statistics def namespace_statistics
@namespace.namespace_statistics || @namespace.create_namespace_statistics strong_memoize(:namespace_statistics) do
NamespaceStatistics.safe_find_or_create_by!(namespace_id: @namespace_id)
rescue ActiveRecord::NotNullViolation, ActiveRecord::RecordInvalid
end
end end
def project_statistics def project_statistics
@project.statistics || @project.create_statistics(namespace: @project.namespace) strong_memoize(:project_statistics) do
ProjectStatistics.safe_find_or_create_by!(project_id: @project_id)
rescue ActiveRecord::NotNullViolation, ActiveRecord::RecordInvalid
end
end end
end end
end end
......
...@@ -791,6 +791,15 @@ ...@@ -791,6 +791,15 @@
:weight: 1 :weight: 1
:idempotent: :idempotent:
:tags: [] :tags: []
- :name: pipeline_background:ci_minutes_update_project_and_namespace_usage
:worker_name: Ci::Minutes::UpdateProjectAndNamespaceUsageWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:tags: []
- :name: pipeline_background:ci_sync_reports_to_report_approval_rules - :name: pipeline_background:ci_sync_reports_to_report_approval_rules
:worker_name: Ci::SyncReportsToReportApprovalRulesWorker :worker_name: Ci::SyncReportsToReportApprovalRulesWorker
:feature_category: :continuous_integration :feature_category: :continuous_integration
......
# frozen_string_literal: true
module Ci
module Minutes
class UpdateProjectAndNamespaceUsageWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
include PipelineBackgroundQueue
urgency :low
data_consistency :always # primarily performs writes
def perform(consumption, project_id, namespace_id)
::Ci::Minutes::UpdateProjectAndNamespaceUsageService.new(project_id, namespace_id).execute(consumption)
end
end
end
end
...@@ -4,7 +4,7 @@ module EE ...@@ -4,7 +4,7 @@ module EE
module Ci module Ci
module BuildFinishedWorker module BuildFinishedWorker
def process_build(build) def process_build(build)
unless ::Feature.enabled?(:cancel_pipelines_prior_to_destroy, default_enabled: :yaml) unless ::Feature.enabled?(:cancel_pipelines_prior_to_destroy, build.project, default_enabled: :yaml)
::Ci::Minutes::UpdateBuildMinutesService.new(build.project, nil).execute(build) ::Ci::Minutes::UpdateBuildMinutesService.new(build.project, nil).execute(build)
end end
......
...@@ -22,7 +22,7 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do ...@@ -22,7 +22,7 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do
end end
describe '.find_or_create_current' do describe '.find_or_create_current' do
subject { described_class.find_or_create_current(namespace) } subject { described_class.find_or_create_current(namespace_id: namespace.id) }
shared_examples 'creates usage record' do shared_examples 'creates usage record' do
it 'creates new record and resets minutes consumption' do it 'creates new record and resets minutes consumption' do
......
...@@ -22,7 +22,7 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do ...@@ -22,7 +22,7 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do
end end
describe '.find_or_create_current' do describe '.find_or_create_current' do
subject { described_class.find_or_create_current(project) } subject { described_class.find_or_create_current(project_id: project.id) }
shared_examples 'creates usage record' do shared_examples 'creates usage record' do
it 'creates new record and resets minutes consumption' do it 'creates new record and resets minutes consumption' do
......
...@@ -3,28 +3,28 @@ ...@@ -3,28 +3,28 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::Minutes::UpdateBuildMinutesService do RSpec.describe Ci::Minutes::UpdateBuildMinutesService do
describe '#perform' do let(:namespace) { create(:namespace, shared_runners_minutes_limit: 100) }
let(:namespace) { create(:namespace, shared_runners_minutes_limit: 100) } let(:project) { create(:project, :private, namespace: namespace) }
let(:project) { create(:project, :private, namespace: namespace) } let(:pipeline) { create(:ci_pipeline, project: project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) do
let(:build) do create(:ci_build, :success,
create(:ci_build, :success, runner: runner, pipeline: pipeline,
runner: runner, pipeline: pipeline, started_at: 2.hours.ago, finished_at: 1.hour.ago)
started_at: 2.hours.ago, finished_at: 1.hour.ago) end
end
let(:namespace_amount_used) { Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(namespace).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).amount_used } let(:project_amount_used) { Ci::Minutes::ProjectMonthlyUsage.find_or_create_current(project_id: project.id).amount_used }
subject { described_class.new(project, nil).execute(build) } subject { described_class.new(project, nil).execute(build) }
shared_examples 'executes service' do
shared_examples 'new tracking matches legacy tracking' do shared_examples 'new tracking matches legacy tracking' do
it 'stores the same information in both legacy and new tracking' do it 'stores the same information in both legacy and new tracking' do
subject subject
expect(namespace_amount_used) expect(namespace_amount_used)
.to eq((namespace.namespace_statistics.reload.shared_runners_seconds.to_f / 60).round(2)) .to eq((namespace.reload.namespace_statistics.shared_runners_seconds.to_f / 60).round(2))
expect(project_amount_used) expect(project_amount_used)
.to eq((project.statistics.reload.shared_runners_seconds.to_f / 60).round(2)) .to eq((project.statistics.reload.shared_runners_seconds.to_f / 60).round(2))
...@@ -181,7 +181,7 @@ RSpec.describe Ci::Minutes::UpdateBuildMinutesService do ...@@ -181,7 +181,7 @@ RSpec.describe Ci::Minutes::UpdateBuildMinutesService do
let(:root_ancestor) { create(:group, shared_runners_minutes_limit: 100) } let(:root_ancestor) { create(:group, shared_runners_minutes_limit: 100) }
let(:namespace) { create(:group, parent: root_ancestor) } let(:namespace) { create(:group, parent: root_ancestor) }
let(:namespace_amount_used) { Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(root_ancestor).amount_used } let(:namespace_amount_used) { Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(namespace_id: root_ancestor.id).amount_used }
it 'creates a statistics in root group' do it 'creates a statistics in root group' do
subject subject
...@@ -253,4 +253,18 @@ RSpec.describe Ci::Minutes::UpdateBuildMinutesService do ...@@ -253,4 +253,18 @@ RSpec.describe Ci::Minutes::UpdateBuildMinutesService do
it_behaves_like 'does nothing' it_behaves_like 'does nothing'
end end
end end
describe '#execute' do
context 'when cancel_pipelines_prior_to_destroy enabled', :sidekiq_inline do
include_examples 'executes service'
end
context 'when cancel_pipelines_prior_to_destroy disabled' do
before do
stub_feature_flags(cancel_pipelines_prior_to_destroy: false)
end
include_examples 'executes service'
end
end
end end
...@@ -3,16 +3,16 @@ ...@@ -3,16 +3,16 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageService do RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageService do
let_it_be(:namespace) { create(:namespace, shared_runners_minutes_limit: 100) } let_it_be(:project) { create(:project, :private) }
let_it_be(:project) { create(:project, :private, namespace: namespace) } let_it_be(:namespace) { project.namespace }
let(:consumption_minutes) { 120 } let(:consumption_minutes) { 120 }
let(:consumption_seconds) { consumption_minutes * 60 } let(:consumption_seconds) { consumption_minutes * 60 }
let(:namespace_amount_used) { Ci::Minutes::NamespaceMonthlyUsage.find_or_create_current(namespace).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).amount_used } let(:project_amount_used) { Ci::Minutes::ProjectMonthlyUsage.find_or_create_current(project_id: project.id).amount_used }
describe '#execute' do describe '#execute' do
subject { described_class.new(project, namespace).execute(consumption_minutes) } subject { described_class.new(project.id, namespace.id).execute(consumption_minutes) }
context 'with shared runner' do context 'with shared runner' do
context 'when statistics and usage do not have existing values' do context 'when statistics and usage do not have existing values' do
...@@ -26,6 +26,46 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageService do ...@@ -26,6 +26,46 @@ RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageService do
.to eq(consumption_seconds) .to eq(consumption_seconds)
end end
context 'when project deleted' do
let(:project) { double(id: non_existing_record_id) }
it 'will complete successfully and increment namespace statistics' do
subject
expect(ProjectStatistics.find_by_project_id(project.id)).to be_nil
expect(NamespaceStatistics.find_by_namespace_id(namespace.id).shared_runners_seconds).to eq(consumption_seconds)
expect(Ci::Minutes::ProjectMonthlyUsage.find_by_project_id(project.id)).to be_nil
expect(Ci::Minutes::NamespaceMonthlyUsage.find_by_namespace_id(namespace.id).amount_used).to eq(consumption_minutes)
end
end
context 'when namespace deleted' do
let(:namespace) { double(id: non_existing_record_id) }
it 'will complete successfully' do
subject
expect(ProjectStatistics.find_by_project_id(project.id).shared_runners_seconds).to eq(consumption_seconds)
expect(NamespaceStatistics.find_by_namespace_id(namespace.id)).to be_nil
expect(Ci::Minutes::ProjectMonthlyUsage.find_by_project_id(project.id).amount_used).to eq(consumption_minutes)
expect(Ci::Minutes::NamespaceMonthlyUsage.find_by_namespace_id(namespace.id).amount_used).to eq(consumption_minutes)
end
end
context 'when project and namespace deleted' do
let(:project) { double(id: non_existing_record_id) }
let(:namespace) { double(id: non_existing_record_id) }
it 'will complete successfully' do
subject
expect(ProjectStatistics.find_by_project_id(project.id)).to be_nil
expect(NamespaceStatistics.find_by_namespace_id(namespace.id)).to be_nil
expect(Ci::Minutes::ProjectMonthlyUsage.find_by_project_id(project.id)).to be_nil
expect(Ci::Minutes::NamespaceMonthlyUsage.find_by_namespace_id(namespace.id).amount_used).to eq(consumption_minutes)
end
end
it 'updates monthly usage with consumption minutes' do it 'updates monthly usage with consumption minutes' do
subject subject
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::Minutes::UpdateProjectAndNamespaceUsageWorker do
let_it_be(:project) { create(:project) }
let_it_be(:namespace) { project.namespace }
let(:consumption) { 100 }
let(:consumption_seconds) { consumption * 60 }
let(:worker) { described_class.new }
describe '#perform' do
it 'executes UpdateProjectAndNamespaceUsageService' do
service_instance = double
expect(::Ci::Minutes::UpdateProjectAndNamespaceUsageService).to receive(:new).with(project.id, namespace.id).and_return(service_instance)
expect(service_instance).to receive(:execute).with(consumption)
worker.perform(consumption, project.id, namespace.id)
end
it 'updates statistics and usage' do
worker.perform(consumption, project.id, namespace.id)
expect(project.statistics.reload.shared_runners_seconds).to eq(consumption_seconds)
expect(namespace.namespace_statistics.reload.shared_runners_seconds).to eq(consumption_seconds)
expect(Ci::Minutes::NamespaceMonthlyUsage.find_by(namespace: namespace).amount_used).to eq(consumption)
expect(Ci::Minutes::ProjectMonthlyUsage.find_by(project: project).amount_used).to eq(consumption)
end
it 'accumulates only legacy statistics on failure (behaves transactionally)' do
allow(Ci::Minutes::ProjectMonthlyUsage).to receive(:new).and_raise(StandardError)
expect { worker.perform(consumption, project.id, namespace.id) }.to raise_error(StandardError)
expect(project.reload.statistics.shared_runners_seconds).to eq(consumption_seconds)
expect(namespace.reload.namespace_statistics.shared_runners_seconds).to eq(consumption_seconds)
expect(Ci::Minutes::NamespaceMonthlyUsage.find_by(namespace: namespace)).to eq(nil)
expect(Ci::Minutes::ProjectMonthlyUsage.find_by(project: project)).to eq(nil)
expect(::Ci::Minutes::EmailNotificationService).not_to receive(:new)
end
end
end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Gitlab module Gitlab
module Ci module Ci
## ##
# Ci::Features is a class that aggregates all CI/CD feature flags in one place. # Deprecated: Ci::Features is a class that aggregates all CI/CD feature flags in one place.
# #
module Features module Features
# NOTE: The feature flag `disallow_to_create_merge_request_pipelines_in_target_project` # NOTE: The feature flag `disallow_to_create_merge_request_pipelines_in_target_project`
......
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