Commit 403da044 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '235730-clean-old-pages-deployments-on-new-deployment' into 'master'

Clean old pages deployments on new deployment

See merge request gitlab-org/gitlab!44575
parents 8af3babf 28780fa2
...@@ -7,6 +7,8 @@ class PagesDeployment < ApplicationRecord ...@@ -7,6 +7,8 @@ class PagesDeployment < ApplicationRecord
belongs_to :project, optional: false belongs_to :project, optional: false
belongs_to :ci_build, class_name: 'Ci::Build', optional: true belongs_to :ci_build, class_name: 'Ci::Build', optional: true
scope :older_than, -> (id) { where('id < ?', id) }
validates :file, presence: true validates :file, presence: true
validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES } validates :file_store, presence: true, inclusion: { in: ObjectStorage::SUPPORTED_STORES }
validates :size, presence: true, numericality: { greater_than: 0, only_integer: true } validates :size, presence: true, numericality: { greater_than: 0, only_integer: true }
......
...@@ -346,7 +346,8 @@ class Project < ApplicationRecord ...@@ -346,7 +346,8 @@ class Project < ApplicationRecord
# GitLab Pages # GitLab Pages
has_many :pages_domains has_many :pages_domains
has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project
has_many :pages_deployments # we need to clean up files, not only remove records
has_many :pages_deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# Can be too many records. We need to implement delete_all in batches. # Can be too many records. We need to implement delete_all in batches.
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/228637 # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/228637
...@@ -1801,6 +1802,8 @@ class Project < ApplicationRecord ...@@ -1801,6 +1802,8 @@ class Project < ApplicationRecord
mark_pages_as_not_deployed unless destroyed? mark_pages_as_not_deployed unless destroyed?
DestroyPagesDeploymentsWorker.perform_async(id)
# 1. We rename pages to temporary directory # 1. We rename pages to temporary directory
# 2. We wait 5 minutes, due to NFS caching # 2. We wait 5 minutes, due to NFS caching
# 3. We asynchronously remove pages with force # 3. We asynchronously remove pages with force
...@@ -1817,7 +1820,7 @@ class Project < ApplicationRecord ...@@ -1817,7 +1820,7 @@ class Project < ApplicationRecord
end end
def mark_pages_as_not_deployed def mark_pages_as_not_deployed
ensure_pages_metadatum.update!(deployed: false, artifacts_archive: nil) ensure_pages_metadatum.update!(deployed: false, artifacts_archive: nil, pages_deployment: nil)
end end
def write_repository_config(gl_full_path: full_path) def write_repository_config(gl_full_path: full_path)
......
# frozen_string_literal: true
module Pages
class DestroyDeploymentsService
def initialize(project, last_deployment_id = nil)
@project = project
@last_deployment_id = last_deployment_id
end
def execute
deployments_to_destroy = @project.pages_deployments
deployments_to_destroy = deployments_to_destroy.older_than(@last_deployment_id) if @last_deployment_id
deployments_to_destroy.find_each(&:destroy) # rubocop: disable CodeReuse/ActiveRecord
end
end
end
...@@ -12,6 +12,11 @@ module Projects ...@@ -12,6 +12,11 @@ module Projects
# as it shares the namespace with groups # as it shares the namespace with groups
TMP_EXTRACT_PATH = '@pages.tmp' TMP_EXTRACT_PATH = '@pages.tmp'
# old deployment can be cached by pages daemon
# so we need to give pages daemon some time update cache
# 10 minutes is enough, but 30 feels safer
OLD_DEPLOYMENTS_DESTRUCTION_DELAY = 30.minutes.freeze
attr_reader :build attr_reader :build
def initialize(project, build) def initialize(project, build)
...@@ -128,6 +133,7 @@ module Projects ...@@ -128,6 +133,7 @@ module Projects
entries_count = build.artifacts_metadata_entry("", recursive: true).entries.count entries_count = build.artifacts_metadata_entry("", recursive: true).entries.count
sha256 = build.job_artifacts_archive.file_sha256 sha256 = build.job_artifacts_archive.file_sha256
deployment = nil
File.open(artifacts_path) do |file| File.open(artifacts_path) do |file|
deployment = project.pages_deployments.create!(file: file, deployment = project.pages_deployments.create!(file: file,
file_count: entries_count, file_count: entries_count,
...@@ -135,7 +141,11 @@ module Projects ...@@ -135,7 +141,11 @@ module Projects
project.pages_metadatum.update!(pages_deployment: deployment) project.pages_metadatum.update!(pages_deployment: deployment)
end end
# TODO: schedule old deployment removal https://gitlab.com/gitlab-org/gitlab/-/issues/235730 DestroyPagesDeploymentsWorker.perform_in(
OLD_DEPLOYMENTS_DESTRUCTION_DELAY,
project.id,
deployment.id
)
rescue => e rescue => e
# we don't want to break current pages deployment process if something goes wrong # we don't want to break current pages deployment process if something goes wrong
# TODO: remove this rescue as part of https://gitlab.com/gitlab-org/gitlab/-/issues/245308 # TODO: remove this rescue as part of https://gitlab.com/gitlab-org/gitlab/-/issues/245308
......
...@@ -1425,6 +1425,14 @@ ...@@ -1425,6 +1425,14 @@
:weight: 1 :weight: 1
:idempotent: :idempotent:
:tags: [] :tags: []
- :name: destroy_pages_deployments
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: detect_repository_languages - :name: detect_repository_languages
:feature_category: :source_code_management :feature_category: :source_code_management
:has_external_dependencies: :has_external_dependencies:
......
# frozen_string_literal: true
class DestroyPagesDeploymentsWorker
include ApplicationWorker
idempotent!
loggable_arguments 0, 1
sidekiq_options retry: 3
feature_category :pages
def perform(project_id, last_deployment_id = nil)
project = Project.find_by_id(project_id)
return unless project
::Pages::DestroyDeploymentsService.new(project, last_deployment_id).execute
end
end
...@@ -86,6 +86,8 @@ ...@@ -86,6 +86,8 @@
- 1 - 1
- - design_management_new_version - - design_management_new_version
- 1 - 1
- - destroy_pages_deployments
- 1
- - detect_repository_languages - - detect_repository_languages
- 1 - 1
- - disallow_two_factor_for_group - - disallow_two_factor_for_group
......
...@@ -42,4 +42,17 @@ RSpec.describe PagesDeployment do ...@@ -42,4 +42,17 @@ RSpec.describe PagesDeployment do
deployment = create(:pages_deployment) deployment = create(:pages_deployment)
expect(deployment.size).to eq(deployment.file.size) expect(deployment.size).to eq(deployment.file.size)
end end
describe '.older_than' do
it 'returns deployments with lower id' do
old_deployments = create_list(:pages_deployment, 2)
deployment = create(:pages_deployment)
# new deployment
create(:pages_deployment)
expect(PagesDeployment.older_than(deployment.id)).to eq(old_deployments)
end
end
end end
...@@ -3668,7 +3668,7 @@ RSpec.describe Project, factory_default: :keep do ...@@ -3668,7 +3668,7 @@ RSpec.describe Project, factory_default: :keep do
let(:project) { create(:project) } let(:project) { create(:project) }
before do before do
project.namespace_id = 7 project.namespace_id = project.namespace_id + 1
end end
it { expect(project.parent_changed?).to be_truthy } it { expect(project.parent_changed?).to be_truthy }
...@@ -4219,6 +4219,27 @@ RSpec.describe Project, factory_default: :keep do ...@@ -4219,6 +4219,27 @@ RSpec.describe Project, factory_default: :keep do
expect { project.destroy }.not_to raise_error expect { project.destroy }.not_to raise_error
end end
context 'when there is an old pages deployment' do
let!(:old_deployment_from_another_project) { create(:pages_deployment) }
let!(:old_deployment) { create(:pages_deployment, project: project) }
it 'schedules a destruction of pages deployments' do
expect(DestroyPagesDeploymentsWorker).to(
receive(:perform_async).with(project.id)
)
project.remove_pages
end
it 'removes pages deployments', :sidekiq_inline do
expect do
project.remove_pages
end.to change { PagesDeployment.count }.by(-1)
expect(PagesDeployment.find_by_id(old_deployment.id)).to be_nil
end
end
end end
describe '#remove_export' do describe '#remove_export' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Pages::DestroyDeploymentsService do
let(:project) { create(:project) }
let!(:old_deployments) { create_list(:pages_deployment, 2, project: project) }
let!(:last_deployment) { create(:pages_deployment, project: project) }
let!(:newer_deployment) { create(:pages_deployment, project: project) }
let!(:deployment_from_another_project) { create(:pages_deployment) }
it 'destroys all deployments of the project' do
expect do
described_class.new(project).execute
end.to change { PagesDeployment.count }.by(-4)
expect(deployment_from_another_project.reload).to be
end
it 'destroy only deployments older than last deployment if it is provided' do
expect do
described_class.new(project, last_deployment.id).execute
end.to change { PagesDeployment.count }.by(-2)
expect(last_deployment.reload).to be
expect(newer_deployment.reload).to be
expect(deployment_from_another_project.reload).to be
end
end
...@@ -71,6 +71,29 @@ RSpec.describe Projects::UpdatePagesService do ...@@ -71,6 +71,29 @@ RSpec.describe Projects::UpdatePagesService do
expect(project.pages_metadatum.reload.pages_deployment_id).to eq(deployment.id) expect(project.pages_metadatum.reload.pages_deployment_id).to eq(deployment.id)
end end
context 'when there is an old pages deployment' do
let!(:old_deployment_from_another_project) { create(:pages_deployment) }
let!(:old_deployment) { create(:pages_deployment, project: project) }
it 'schedules a destruction of older deployments' do
expect(DestroyPagesDeploymentsWorker).to(
receive(:perform_in).with(described_class::OLD_DEPLOYMENTS_DESTRUCTION_DELAY,
project.id,
instance_of(Integer))
)
execute
end
it 'removes older deployments', :sidekiq_inline do
expect do
execute
end.not_to change { PagesDeployment.count } # it creates one and deletes one
expect(PagesDeployment.find_by_id(old_deployment.id)).to be_nil
end
end
it 'does not create deployment when zip_pages_deployments feature flag is disabled' do it 'does not create deployment when zip_pages_deployments feature flag is disabled' do
stub_feature_flags(zip_pages_deployments: false) stub_feature_flags(zip_pages_deployments: false)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DestroyPagesDeploymentsWorker do
subject(:worker) { described_class.new }
let(:project) { create(:project) }
let!(:old_deployment) { create(:pages_deployment, project: project) }
let!(:last_deployment) { create(:pages_deployment, project: project) }
let!(:another_deployment) { create(:pages_deployment) }
it "doesn't fail if project is already removed" do
expect do
worker.perform(-1)
end.not_to raise_error
end
it 'can be called without last_deployment_id' do
expect_next_instance_of(::Pages::DestroyDeploymentsService, project, nil) do |service|
expect(service).to receive(:execute).and_call_original
end
expect do
worker.perform(project.id)
end.to change { PagesDeployment.count }.by(-2)
end
it 'calls destroy service' do
expect_next_instance_of(::Pages::DestroyDeploymentsService, project, last_deployment.id) do |service|
expect(service).to receive(:execute).and_call_original
end
expect do
worker.perform(project.id, last_deployment.id)
end.to change { PagesDeployment.count }.by(-1)
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