Commit 32d8f618 authored by Matt Kasa's avatar Matt Kasa

Use new locked column for Ci::JobArtifacts::DestroyAllExpiredService

Fixes https://gitlab.com/gitlab-org/gitlab/-/issues/327281
parent dfa79015
...@@ -24,21 +24,33 @@ module Ci ...@@ -24,21 +24,33 @@ module Ci
# which is scheduled every 7 minutes. # which is scheduled every 7 minutes.
def execute def execute
in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
if ::Feature.enabled?(:ci_destroy_unlocked_job_artifacts)
destroy_unlocked_job_artifacts(Time.current)
else
destroy_job_artifacts_with_slow_iteration(Time.current) destroy_job_artifacts_with_slow_iteration(Time.current)
end end
end
@removed_artifacts_count @removed_artifacts_count
end end
private private
def destroy_unlocked_job_artifacts(start_at)
loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
artifacts = Ci::JobArtifact.expired_before(start_at).artifact_unlocked.limit(BATCH_SIZE)
service_response = destroy_batch(artifacts)
@removed_artifacts_count += service_response[:destroyed_artifacts_count]
end
end
def destroy_job_artifacts_with_slow_iteration(start_at) def destroy_job_artifacts_with_slow_iteration(start_at)
Ci::JobArtifact.expired_before(start_at).each_batch(of: BATCH_SIZE, column: :expire_at, order: :desc) do |relation, index| Ci::JobArtifact.expired_before(start_at).each_batch(of: BATCH_SIZE, column: :expire_at, order: :desc) do |relation, index|
# For performance reasons, join with ci_pipelines after the batch is queried. # For performance reasons, join with ci_pipelines after the batch is queried.
# See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47496 # See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47496
artifacts = relation.unlocked artifacts = relation.unlocked
service_response = destroy_batch_async(artifacts) service_response = destroy_batch(artifacts)
@removed_artifacts_count += service_response[:destroyed_artifacts_count] @removed_artifacts_count += service_response[:destroyed_artifacts_count]
break if loop_timeout?(start_at) break if loop_timeout?(start_at)
...@@ -46,7 +58,7 @@ module Ci ...@@ -46,7 +58,7 @@ module Ci
end end
end end
def destroy_batch_async(artifacts) def destroy_batch(artifacts)
Ci::JobArtifacts::DestroyBatchService.new(artifacts).execute Ci::JobArtifacts::DestroyBatchService.new(artifacts).execute
end end
......
...@@ -34,7 +34,7 @@ module Ci ...@@ -34,7 +34,7 @@ module Ci
# This is executed outside of the transaction because it depends on Redis # This is executed outside of the transaction because it depends on Redis
update_project_statistics! if update_stats update_project_statistics! if update_stats
increment_monitoring_statistics(artifacts_count) increment_monitoring_statistics(artifacts_count, artifacts_bytes)
success(destroyed_artifacts_count: artifacts_count, success(destroyed_artifacts_count: artifacts_count,
statistics_updates: affected_project_statistics) statistics_updates: affected_project_statistics)
...@@ -63,8 +63,9 @@ module Ci ...@@ -63,8 +63,9 @@ module Ci
end end
end end
def increment_monitoring_statistics(size) def increment_monitoring_statistics(size, bytes)
metrics.increment_destroyed_artifacts(size) metrics.increment_destroyed_artifacts_count(size)
metrics.increment_destroyed_artifacts_bytes(bytes)
end end
def metrics def metrics
...@@ -76,6 +77,12 @@ module Ci ...@@ -76,6 +77,12 @@ module Ci
@job_artifacts.count @job_artifacts.count
end end
end end
def artifacts_bytes
strong_memoize(:artifacts_bytes) do
@job_artifacts.sum { |artifact| artifact.try(:size) || 0 }
end
end
end end
end end
end end
......
---
name: ci_destroy_unlocked_job_artifacts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72406
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338165
milestone: '14.5'
type: development
group: group::testing
default_enabled: false
...@@ -10,53 +10,48 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -10,53 +10,48 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
let(:service) { described_class.new } let(:service) { described_class.new }
let_it_be(:artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) } let_it_be(:locked_pipeline) { create(:ci_pipeline, :artifacts_locked) }
let_it_be(:security_scan) { create(:security_scan, build: artifact.job) } let_it_be(:pipeline) { create(:ci_pipeline, :unlocked) }
let_it_be(:locked_job) { create(:ci_build, :success, pipeline: locked_pipeline) }
let_it_be(:job) { create(:ci_build, :success, pipeline: pipeline) }
let_it_be(:security_scan) { create(:security_scan, build: job) }
let_it_be(:security_finding) { create(:security_finding, scan: security_scan) } let_it_be(:security_finding) { create(:security_finding, scan: security_scan) }
before(:all) do
artifact.job.pipeline.unlocked!
end
context 'when artifact is expired' do context 'when artifact is expired' do
context 'when artifact is not locked' do context 'when artifact is not locked' do
let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
it 'destroys job artifact and the security finding' do it 'destroys job artifact and the security finding' do
expect { subject }.to change { Ci::JobArtifact.count }.by(-1) expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
.and change { Security::Finding.count }.from(1).to(0) .and change { Security::Finding.count }.by(-1)
end end
end end
context 'when artifact is locked' do context 'when artifact is locked' do
before do let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }
artifact.job.pipeline.reload.artifacts_locked!
end
it 'does not destroy job artifact' do it 'does not destroy job artifact' do
expect { subject }.to not_change { Ci::JobArtifact.count } expect { subject }.to not_change { Ci::JobArtifact.count }
.and not_change { Security::Finding.count }.from(1) .and not_change { Security::Finding.count }
end end
end end
end end
context 'when artifact is not expired' do context 'when artifact is not expired' do
before do let!(:artifact) { create(:ci_job_artifact, job: job, locked: job.pipeline.locked) }
artifact.update_column(:expire_at, 1.day.since)
end
it 'does not destroy expired job artifacts' do it 'does not destroy expired job artifacts' do
expect { subject }.to not_change { Ci::JobArtifact.count } expect { subject }.to not_change { Ci::JobArtifact.count }
.and not_change { Security::Finding.count }.from(1) .and not_change { Security::Finding.count }
end end
end end
context 'when artifact is permanent' do context 'when artifact is permanent' do
before do let!(:artifact) { create(:ci_job_artifact, expire_at: nil, job: job, locked: job.pipeline.locked) }
artifact.update_column(:expire_at, nil)
end
it 'does not destroy expired job artifacts' do it 'does not destroy expired job artifacts' do
expect { subject }.to not_change { Ci::JobArtifact.count } expect { subject }.to not_change { Ci::JobArtifact.count }
.and not_change { Security::Finding.count }.from(1) .and not_change { Security::Finding.count }
end end
end end
...@@ -69,41 +64,38 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -69,41 +64,38 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
.and_raise(ActiveRecord::RecordNotDestroyed) .and_raise(ActiveRecord::RecordNotDestroyed)
end end
let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
it 'raises an exception and stop destroying' do it 'raises an exception and stop destroying' do
expect { subject }.to raise_error(ActiveRecord::RecordNotDestroyed) expect { subject }.to raise_error(ActiveRecord::RecordNotDestroyed)
.and not_change { Security::Finding.count }.from(1) .and not_change { Security::Finding.count }
end end
end end
context 'when there are artifacts more than batch sizes' do context 'when there are artifacts more than batch sizes' do
before do before do
stub_const('Ci::JobArtifacts::DestroyAllExpiredService::BATCH_SIZE', 1) stub_const('Ci::JobArtifacts::DestroyAllExpiredService::BATCH_SIZE', 1)
second_artifact.job.pipeline.unlocked!
end end
let!(:second_artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) } let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
let!(:second_security_scan) { create(:security_scan, build: second_artifact.job) } let!(:second_job) { create(:ci_build, :success, pipeline: pipeline) }
let!(:second_artifact) { create(:ci_job_artifact, :expired, job: second_job, locked: second_job.pipeline.locked) }
let!(:second_security_scan) { create(:security_scan, build: second_job) }
let!(:second_security_finding) { create(:security_finding, scan: second_security_scan) } let!(:second_security_finding) { create(:security_finding, scan: second_security_scan) }
it 'destroys all expired artifacts' do it 'destroys all expired artifacts' do
expect { subject }.to change { Ci::JobArtifact.count }.by(-2) expect { subject }.to change { Ci::JobArtifact.count }.by(-2)
.and change { Security::Finding.count }.from(2).to(0) .and change { Security::Finding.count }.by(-2)
end end
end end
context 'when some artifacts are locked' do context 'when some artifacts are locked' do
before do let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
pipeline = create(:ci_pipeline, locked: :artifacts_locked) let!(:second_artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }
job = create(:ci_build, pipeline: pipeline)
create(:ci_job_artifact, expire_at: 1.day.ago, job: job)
security_scan = create(:security_scan, build: job)
create(:security_finding, scan: security_scan)
end
it 'destroys only unlocked artifacts' do it 'destroys only unlocked artifacts' do
expect { subject }.to change { Ci::JobArtifact.count }.by(-1) expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
.and change { Security::Finding.count }.from(2).to(1) .and change { Security::Finding.count }.by(-1)
end end
end end
end end
......
...@@ -6,10 +6,14 @@ module Gitlab ...@@ -6,10 +6,14 @@ module Gitlab
class Metrics class Metrics
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
def increment_destroyed_artifacts(size) def increment_destroyed_artifacts_count(size)
destroyed_artifacts_counter.increment({}, size.to_i) destroyed_artifacts_counter.increment({}, size.to_i)
end end
def increment_destroyed_artifacts_bytes(bytes)
destroyed_artifacts_bytes_counter.increment({}, bytes)
end
private private
def destroyed_artifacts_counter def destroyed_artifacts_counter
...@@ -20,6 +24,15 @@ module Gitlab ...@@ -20,6 +24,15 @@ module Gitlab
::Gitlab::Metrics.counter(name, comment) ::Gitlab::Metrics.counter(name, comment)
end end
end end
def destroyed_artifacts_bytes_counter
strong_memoize(:destroyed_artifacts_bytes_counter) do
name = :destroyed_job_artifacts_bytes_total
comment = 'Counter of bytes of destroyed expired job artifacts'
::Gitlab::Metrics.counter(name, comment)
end
end
end end
end end
end end
......
...@@ -10,9 +10,9 @@ RSpec.describe Gitlab::Ci::Artifacts::Metrics, :prometheus do ...@@ -10,9 +10,9 @@ RSpec.describe Gitlab::Ci::Artifacts::Metrics, :prometheus do
let(:counter) { metrics.send(:destroyed_artifacts_counter) } let(:counter) { metrics.send(:destroyed_artifacts_counter) }
it 'increments a single counter' do it 'increments a single counter' do
subject.increment_destroyed_artifacts(10) subject.increment_destroyed_artifacts_count(10)
subject.increment_destroyed_artifacts(20) subject.increment_destroyed_artifacts_count(20)
subject.increment_destroyed_artifacts(30) subject.increment_destroyed_artifacts_count(30)
expect(counter.get).to eq 60 expect(counter.get).to eq 60
expect(counter.values.count).to eq 1 expect(counter.values.count).to eq 1
......
...@@ -16,13 +16,18 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -16,13 +16,18 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
let_it_be(:job) { create(:ci_build, :success, pipeline: pipeline) } let_it_be(:job) { create(:ci_build, :success, pipeline: pipeline) }
context 'when artifact is expired' do context 'when artifact is expired' do
let!(:artifact) { create(:ci_job_artifact, :expired, job: job) } let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
context 'with preloaded relationships' do context 'with preloaded relationships' do
before do before do
stub_const("#{described_class}::LOOP_LIMIT", 1) stub_const("#{described_class}::LOOP_LIMIT", 1)
end end
context 'with ci_destroy_unlocked_job_artifacts feature flag disabled' do
before do
stub_feature_flags(ci_destroy_unlocked_job_artifacts: false)
end
it 'performs the smallest number of queries for job_artifacts' do it 'performs the smallest number of queries for job_artifacts' do
log = ActiveRecord::QueryRecorder.new { subject } log = ActiveRecord::QueryRecorder.new { subject }
...@@ -39,6 +44,18 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -39,6 +44,18 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end end
end end
context 'with ci_destroy_unlocked_job_artifacts feature flag enabled' do
before do
stub_feature_flags(ci_destroy_unlocked_job_artifacts: true)
end
it 'performs the smallest number of queries for job_artifacts' do
log = ActiveRecord::QueryRecorder.new { subject }
expect(log.count).to be_within(1).of(8)
end
end
end
context 'when artifact is not locked' do context 'when artifact is not locked' do
it 'deletes job artifact record' do it 'deletes job artifact record' do
expect { subject }.to change { Ci::JobArtifact.count }.by(-1) expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
...@@ -53,7 +70,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -53,7 +70,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end end
context 'when the artifact has a file attached to it' do context 'when the artifact has a file attached to it' do
let!(:artifact) { create(:ci_job_artifact, :expired, :zip, job: job) } let!(:artifact) { create(:ci_job_artifact, :expired, :zip, job: job, locked: job.pipeline.locked) }
it 'creates a deleted object' do it 'creates a deleted object' do
expect { subject }.to change { Ci::DeletedObject.count }.by(1) expect { subject }.to change { Ci::DeletedObject.count }.by(1)
...@@ -74,7 +91,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -74,7 +91,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end end
context 'when artifact is locked' do context 'when artifact is locked' do
let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job) } let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }
it 'does not destroy job artifact' do it 'does not destroy job artifact' do
expect { subject }.not_to change { Ci::JobArtifact.count } expect { subject }.not_to change { Ci::JobArtifact.count }
...@@ -83,7 +100,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -83,7 +100,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end end
context 'when artifact is not expired' do context 'when artifact is not expired' do
let!(:artifact) { create(:ci_job_artifact, job: job) } let!(:artifact) { create(:ci_job_artifact, job: job, locked: job.pipeline.locked) }
it 'does not destroy expired job artifacts' do it 'does not destroy expired job artifacts' do
expect { subject }.not_to change { Ci::JobArtifact.count } expect { subject }.not_to change { Ci::JobArtifact.count }
...@@ -91,7 +108,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -91,7 +108,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end end
context 'when artifact is permanent' do context 'when artifact is permanent' do
let!(:artifact) { create(:ci_job_artifact, expire_at: nil, job: job) } let!(:artifact) { create(:ci_job_artifact, expire_at: nil, job: job, locked: job.pipeline.locked) }
it 'does not destroy expired job artifacts' do it 'does not destroy expired job artifacts' do
expect { subject }.not_to change { Ci::JobArtifact.count } expect { subject }.not_to change { Ci::JobArtifact.count }
...@@ -99,7 +116,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -99,7 +116,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end end
context 'when failed to destroy artifact' do context 'when failed to destroy artifact' do
let!(:artifact) { create(:ci_job_artifact, :expired, job: job) } let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
before do before do
stub_const("#{described_class}::LOOP_LIMIT", 10) stub_const("#{described_class}::LOOP_LIMIT", 10)
...@@ -135,7 +152,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -135,7 +152,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end end
context 'when exclusive lease has already been taken by the other instance' do context 'when exclusive lease has already been taken by the other instance' do
let!(:artifact) { create(:ci_job_artifact, :expired, job: job) } let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
before do before do
stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY, timeout: described_class::LOCK_TIMEOUT) stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY, timeout: described_class::LOCK_TIMEOUT)
...@@ -149,8 +166,8 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -149,8 +166,8 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
context 'with a second artifact and batch size of 1' do context 'with a second artifact and batch size of 1' do
let(:second_job) { create(:ci_build, :success, pipeline: pipeline) } let(:second_job) { create(:ci_build, :success, pipeline: pipeline) }
let!(:second_artifact) { create(:ci_job_artifact, :archive, expire_at: 1.day.ago, job: second_job) } let!(:second_artifact) { create(:ci_job_artifact, :archive, expire_at: 1.day.ago, job: second_job, locked: job.pipeline.locked) }
let!(:artifact) { create(:ci_job_artifact, :expired, job: job) } let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
before do before do
stub_const("#{described_class}::BATCH_SIZE", 1) stub_const("#{described_class}::BATCH_SIZE", 1)
...@@ -206,8 +223,8 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -206,8 +223,8 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end end
context 'when some artifacts are locked' do context 'when some artifacts are locked' do
let!(:artifact) { create(:ci_job_artifact, :expired, job: job) } let!(:artifact) { create(:ci_job_artifact, :expired, job: job, locked: job.pipeline.locked) }
let!(:locked_artifact) { create(:ci_job_artifact, :expired, job: locked_job) } let!(:locked_artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }
it 'destroys only unlocked artifacts' do it 'destroys only unlocked artifacts' do
expect { subject }.to change { Ci::JobArtifact.count }.by(-1) expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
...@@ -216,7 +233,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s ...@@ -216,7 +233,7 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s
end end
context 'when all artifacts are locked' do context 'when all artifacts are locked' do
let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job) } let!(:artifact) { create(:ci_job_artifact, :expired, job: locked_job, locked: locked_job.pipeline.locked) }
it 'destroys no artifacts' do it 'destroys no artifacts' do
expect { subject }.to change { Ci::JobArtifact.count }.by(0) expect { subject }.to change { Ci::JobArtifact.count }.by(0)
......
...@@ -29,7 +29,8 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do ...@@ -29,7 +29,8 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
it 'reports metrics for destroyed artifacts' do it 'reports metrics for destroyed artifacts' do
expect_next_instance_of(Gitlab::Ci::Artifacts::Metrics) do |metrics| expect_next_instance_of(Gitlab::Ci::Artifacts::Metrics) do |metrics|
expect(metrics).to receive(:increment_destroyed_artifacts).with(1).and_call_original expect(metrics).to receive(:increment_destroyed_artifacts_count).with(1).and_call_original
expect(metrics).to receive(:increment_destroyed_artifacts_bytes).with(107464).and_call_original
end end
execute execute
......
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