Commit 28780fa2 authored by Vladimir Shushlin's avatar Vladimir Shushlin Committed by Kamil Trzciński

Destroy pages deployments

Add worker for destroying pages deployments

Schedule DestroyPagesdeploymentsworker on new deployment

Remove pages deployment when pages are destroyed
parent 451fbf0c
...@@ -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