Commit 0203c5dd authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Add admin page for batched background migrations

Adds a page for showing the progress of batched background migrations

Changelog: added
MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60911
parent 45cf5194
# frozen_string_literal: true
class Admin::BackgroundMigrationsController < Admin::ApplicationController
feature_category :database
def index
@relations_by_tab = {
'queued' => batched_migration_class.queued.queue_order,
'failed' => batched_migration_class.failed.queue_order,
'finished' => batched_migration_class.finished.queue_order.reverse_order
}
@current_tab = @relations_by_tab.key?(params[:tab]) ? params[:tab] : 'queued'
@migrations = @relations_by_tab[@current_tab].page(params[:page])
@successful_rows_counts = batched_migration_class.successful_rows_counts(@migrations.map(&:id))
end
private
def batched_migration_class
Gitlab::Database::BackgroundMigration::BatchedMigration
end
end
# frozen_string_literal: true
module Admin
module BackgroundMigrationsHelper
def batched_migration_status_badge_class_name(migration)
class_names = {
'active' => 'badge-info',
'paused' => 'badge-warning',
'failed' => 'badge-danger',
'finished' => 'badge-success'
}
class_names[migration.status]
end
# The extra logic here is needed because total_tuple_count is just
# an estimate and completed_rows also does not account for last jobs
# whose batch size is likely larger than the actual number of rows processed
def batched_migration_progress(migration, completed_rows)
return 100 if migration.finished?
return 0 unless completed_rows.to_i > 0
return unless migration.total_tuple_count.to_i > 0
[100 * completed_rows / migration.total_tuple_count, 99].min
end
end
end
...@@ -61,7 +61,7 @@ module NavHelper ...@@ -61,7 +61,7 @@ module NavHelper
end end
def admin_monitoring_nav_links def admin_monitoring_nav_links
%w(system_info background_jobs health_check requests_profiles) %w(system_info background_migrations background_jobs health_check requests_profiles)
end end
def admin_analytics_nav_links def admin_analytics_nav_links
......
%tr{ role: 'row' }
%td{ role: 'cell', data: { label: _('Migration') } }= migration.job_class_name + ': ' + migration.table_name
%td{ role: 'cell', data: { label: _('Progress') } }
- progress = batched_migration_progress(migration, @successful_rows_counts[migration.id])
- if progress
= number_to_percentage(progress, precision: 2)
- else
= _('Unknown')
%td{ role: 'cell', data: { label: _('Status') } }
%span.badge.badge-pill.gl-badge.sm{ class: batched_migration_status_badge_class_name(migration) }= migration.status.humanize
- page_title _('Background Migrations')
.tabs.gl-tabs
%div
%ul.nav.gl-tabs-nav{ role: 'tablist' }
- active_tab_classes = ['gl-tab-nav-item-active', 'gl-tab-nav-item-active-indigo']
%li.nav-item{ role: 'presentation' }
%a.nav-link.gl-tab-nav-item{ href: admin_background_migrations_path, class: (active_tab_classes if @current_tab == 'queued'), role: 'tab' }
= _('Queued')
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
= limited_counter_with_delimiter(@relations_by_tab['queued'])
%li.nav-item{ role: 'presentation' }
%a.nav-link.gl-tab-nav-item{ href: admin_background_migrations_path(tab: 'failed'), class: (active_tab_classes if @current_tab == 'failed'), role: 'tab' }
= _('Failed')
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
= limited_counter_with_delimiter(@relations_by_tab['failed'])
%li.nav-item{ role: 'presentation' }
%a.nav-link.gl-tab-nav-item{ href: admin_background_migrations_path(tab: 'finished'), class: (active_tab_classes if @current_tab == 'finished'), role: 'tab' }
= _('Finished')
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
= limited_counter_with_delimiter(@relations_by_tab['finished'])
.tab-content.gl-tab-content
.tab-pane.active{ role: 'tabpanel' }
%table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' }
%thead{ role: 'rowgroup' }
%tr{ role: 'row' }
%th.table-th-transparent.border-bottom{ role: 'cell' }= _('Migration')
%th.table-th-transparent.border-bottom{ role: 'cell' }= _('Progress')
%th.table-th-transparent.border-bottom{ role: 'cell' }= _('Status')
%tbody{ role: 'rowgroup' }
= render partial: 'migration', collection: @migrations
= paginate_collection @migrations
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
= _('Monitoring') = _('Monitoring')
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_monitoring_submenu_content' } } %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_monitoring_submenu_content' } }
= nav_link(controller: %w(system_info background_jobs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do = nav_link(controller: admin_monitoring_nav_links, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_system_info_path do = link_to admin_system_info_path do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('Monitoring') = _('Monitoring')
...@@ -87,6 +87,10 @@ ...@@ -87,6 +87,10 @@
= link_to admin_system_info_path, title: _('System Info') do = link_to admin_system_info_path, title: _('System Info') do
%span %span
= _('System Info') = _('System Info')
= nav_link(controller: :background_migrations) do
= link_to admin_background_migrations_path, title: _('Background Migrations') do
%span
= _('Background Migrations')
= nav_link(controller: :background_jobs) do = nav_link(controller: :background_jobs) do
= link_to admin_background_jobs_path, title: _('Background Jobs') do = link_to admin_background_jobs_path, title: _('Background Jobs') do
%span %span
......
---
title: Add admin page for batched background migrations
merge_request: 60911
author:
type: added
...@@ -95,6 +95,7 @@ namespace :admin do ...@@ -95,6 +95,7 @@ namespace :admin do
get :instance_review, to: 'instance_review#index' get :instance_review, to: 'instance_review#index'
resources :background_migrations, only: [:index]
resource :health_check, controller: 'health_check', only: [:show] resource :health_check, controller: 'health_check', only: [:show]
resource :background_jobs, controller: 'background_jobs', only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show]
......
...@@ -30,7 +30,7 @@ module Gitlab ...@@ -30,7 +30,7 @@ module Gitlab
scope :successful_in_execution_order, -> { where.not(finished_at: nil).succeeded.order(:finished_at) } scope :successful_in_execution_order, -> { where.not(finished_at: nil).succeeded.order(:finished_at) }
delegate :aborted?, :job_class, :table_name, :column_name, :job_arguments, delegate :job_class, :table_name, :column_name, :job_arguments,
to: :batched_migration, prefix: :migration to: :batched_migration, prefix: :migration
attribute :pause_ms, :integer, default: 100 attribute :pause_ms, :integer, default: 100
......
...@@ -15,11 +15,11 @@ module Gitlab ...@@ -15,11 +15,11 @@ module Gitlab
foreign_key: :batched_background_migration_id foreign_key: :batched_background_migration_id
scope :queue_order, -> { order(id: :asc) } scope :queue_order, -> { order(id: :asc) }
scope :queued, -> { where(status: [:active, :paused]) }
enum status: { enum status: {
paused: 0, paused: 0,
active: 1, active: 1,
aborted: 2,
finished: 3, finished: 3,
failed: 4 failed: 4
} }
...@@ -30,6 +30,14 @@ module Gitlab ...@@ -30,6 +30,14 @@ module Gitlab
active.queue_order.first active.queue_order.first
end end
def self.successful_rows_counts(migrations)
BatchedJob
.succeeded
.where(batched_background_migration_id: migrations)
.group(:batched_background_migration_id)
.sum(:batch_size)
end
def interval_elapsed?(variance: 0) def interval_elapsed?(variance: 0)
return true unless last_job return true unless last_job
......
...@@ -4871,6 +4871,9 @@ msgstr "" ...@@ -4871,6 +4871,9 @@ msgstr ""
msgid "Background Jobs" msgid "Background Jobs"
msgstr "" msgstr ""
msgid "Background Migrations"
msgstr ""
msgid "Background color" msgid "Background color"
msgstr "" msgstr ""
...@@ -21146,6 +21149,9 @@ msgstr "" ...@@ -21146,6 +21149,9 @@ msgstr ""
msgid "Migrated %{success_count}/%{total_count} files." msgid "Migrated %{success_count}/%{total_count} files."
msgstr "" msgstr ""
msgid "Migration"
msgstr ""
msgid "Migration has been scheduled to be retried" msgid "Migration has been scheduled to be retried"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe "Admin > Admin sees background migrations" do
let_it_be(:admin) { create(:admin) }
let_it_be(:active_migration) { create(:batched_background_migration, table_name: 'active', status: :active) }
let_it_be(:failed_migration) { create(:batched_background_migration, table_name: 'failed', status: :failed, total_tuple_count: 100) }
let_it_be(:finished_migration) { create(:batched_background_migration, table_name: 'finished', status: :finished) }
before_all do
create(:batched_background_migration_job, batched_migration: failed_migration, batch_size: 30, status: :succeeded)
end
before do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
end
it 'can navigate to background migrations' do
visit admin_root_path
within '.nav-sidebar' do
link = find_link 'Background Migrations'
link.click
expect(page).to have_current_path(admin_background_migrations_path)
expect(link).to have_ancestor(:css, 'li.active')
end
end
it 'can view queued migrations' do
visit admin_background_migrations_path
within '#content-body' do
expect(page).to have_selector('tbody tr', count: 1)
expect(page).to have_content(active_migration.job_class_name)
expect(page).to have_content(active_migration.table_name)
expect(page).to have_content('0.00%')
expect(page).to have_content(active_migration.status.humanize)
end
end
it 'can view failed migrations' do
visit admin_background_migrations_path
within '#content-body' do
tab = find_link 'Failed'
tab.click
expect(page).to have_current_path(admin_background_migrations_path(tab: 'failed'))
expect(tab[:class]).to include('gl-tab-nav-item-active', 'gl-tab-nav-item-active-indigo')
expect(page).to have_selector('tbody tr', count: 1)
expect(page).to have_content(failed_migration.job_class_name)
expect(page).to have_content(failed_migration.table_name)
expect(page).to have_content('30.00%')
expect(page).to have_content(failed_migration.status.humanize)
end
end
it 'can view finished migrations' do
visit admin_background_migrations_path
within '#content-body' do
tab = find_link 'Finished'
tab.click
expect(page).to have_current_path(admin_background_migrations_path(tab: 'finished'))
expect(tab[:class]).to include('gl-tab-nav-item-active', 'gl-tab-nav-item-active-indigo')
expect(page).to have_selector('tbody tr', count: 1)
expect(page).to have_content(finished_migration.job_class_name)
expect(page).to have_content(finished_migration.table_name)
expect(page).to have_content('100.00%')
expect(page).to have_content(finished_migration.status.humanize)
end
end
end
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Admin::BackgroundMigrationsHelper do
describe '#batched_migration_status_badge_class_name' do
using RSpec::Parameterized::TableSyntax
where(:status, :class_name) do
:active | 'badge-info'
:paused | 'badge-warning'
:failed | 'badge-danger'
:finished | 'badge-success'
end
subject { helper.batched_migration_status_badge_class_name(migration) }
with_them do
let(:migration) { build(:batched_background_migration, status: status) }
it { is_expected.to eq(class_name) }
end
end
describe '#batched_migration_progress' do
subject { helper.batched_migration_progress(migration, completed_rows) }
let(:migration) { build(:batched_background_migration, status: :active, total_tuple_count: 100) }
let(:completed_rows) { 25 }
it 'returns completion percentage' do
expect(subject).to eq(25)
end
context 'when migration is finished' do
let(:migration) { build(:batched_background_migration, status: :finished, total_tuple_count: nil) }
it 'returns 100 percent' do
expect(subject).to eq(100)
end
end
context 'when total_tuple_count is nil' do
let(:migration) { build(:batched_background_migration, status: :active, total_tuple_count: nil) }
it 'returns nil' do
expect(subject).to eq(nil)
end
context 'when there are no completed rows' do
let(:completed_rows) { 0 }
it 'returns 0 percent' do
expect(subject).to eq(0)
end
end
end
context 'when completed rows are greater than total count' do
let(:completed_rows) { 150 }
it 'returns 99 percent' do
expect(subject).to eq(99)
end
end
end
end
...@@ -49,16 +49,6 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d ...@@ -49,16 +49,6 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
let(:batched_job) { build(:batched_background_migration_job) } let(:batched_job) { build(:batched_background_migration_job) }
let(:batched_migration) { batched_job.batched_migration } let(:batched_migration) { batched_job.batched_migration }
describe '#migration_aborted?' do
before do
batched_migration.status = :aborted
end
it 'returns the migration aborted?' do
expect(batched_job.migration_aborted?).to eq(batched_migration.aborted?)
end
end
describe '#migration_job_class' do describe '#migration_job_class' do
it 'returns the migration job_class' do it 'returns the migration job_class' do
expect(batched_job.migration_job_class).to eq(batched_migration.job_class) expect(batched_job.migration_job_class).to eq(batched_migration.job_class)
......
...@@ -36,6 +36,38 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m ...@@ -36,6 +36,38 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
it 'returns the first active migration according to queue order' do it 'returns the first active migration according to queue order' do
expect(described_class.active_migration).to eq(migration2) expect(described_class.active_migration).to eq(migration2)
create(:batched_background_migration_job, batched_migration: migration1, batch_size: 1000, status: :succeeded)
end
end
describe '.queued' do
let!(:migration1) { create(:batched_background_migration, :finished) }
let!(:migration2) { create(:batched_background_migration, :paused) }
let!(:migration3) { create(:batched_background_migration, :active) }
it 'returns active and paused migrations' do
expect(described_class.queued).to contain_exactly(migration2, migration3)
end
end
describe '.successful_rows_counts' do
let!(:migration1) { create(:batched_background_migration) }
let!(:migration2) { create(:batched_background_migration) }
let!(:migration_without_jobs) { create(:batched_background_migration) }
before do
create(:batched_background_migration_job, batched_migration: migration1, batch_size: 1000, status: :succeeded)
create(:batched_background_migration_job, batched_migration: migration1, batch_size: 200, status: :failed)
create(:batched_background_migration_job, batched_migration: migration2, batch_size: 500, status: :succeeded)
create(:batched_background_migration_job, batched_migration: migration2, batch_size: 200, status: :running)
end
it 'returns totals from successful jobs' do
results = described_class.successful_rows_counts([migration1, migration2, migration_without_jobs])
expect(results[migration1.id]).to eq(1000)
expect(results[migration2.id]).to eq(500)
expect(results[migration_without_jobs.id]).to eq(nil)
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