Merge pull request #2525 from okriuchykhin/ok_SCI_4552

Implement background processing of repository snapshots [SCI-4552]
This commit is contained in:
Alex Kriuchykhin 2020-04-29 12:03:23 +02:00 committed by GitHub
commit 0e865f8e45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 222 additions and 132 deletions

View file

@ -3,6 +3,7 @@
var MyModuleRepositories = (function() {
const FULL_VIEW_MODAL = $('#myModuleRepositoryFullViewModal');
const STATUS_POLLING_INTERVAL = 30000;
var SIMPLE_TABLE;
var FULL_VIEW_TABLE;
var FULL_VIEW_TABLE_SCROLLBAR;
@ -165,27 +166,10 @@ var MyModuleRepositories = (function() {
versionsSidebar.find(`[data-id="${currentId}"]`).addClass('active');
}
function createDestroySnapshot(actionPath, requestType) {
animateSpinner(null, true);
$.ajax({
url: actionPath,
type: requestType,
dataType: 'json',
success: function(data) {
FULL_VIEW_MODAL.find('.repository-versions-sidebar').html(data.html);
setSelectedItem();
animateSpinner(null, false);
},
error: function() {
// TODO
}
});
}
function reloadTable(tableUrl) {
animateSpinner(null, true);
if (FULL_VIEW_TABLE) FULL_VIEW_TABLE.destroy();
$.get(tableUrl, (data) => {
$.getJSON(tableUrl, (data) => {
FULL_VIEW_MODAL.find('.table-container').html(data.html);
renderFullViewTable(FULL_VIEW_MODAL.find('.table'));
setSelectedItem();
@ -193,6 +177,20 @@ var MyModuleRepositories = (function() {
});
}
function checkSnapshotStatus(snapshotItem) {
$.getJSON(snapshotItem.data('status-url'), (statusData) => {
if (statusData.status === 'ready') {
$.getJSON(snapshotItem.data('item-url'), (itemData) => {
snapshotItem.replaceWith(itemData.html);
});
} else {
setTimeout(function() {
checkSnapshotStatus(snapshotItem);
}, STATUS_POLLING_INTERVAL);
}
});
}
function initSimpleTable() {
$('#assigned-items-container').on('show.bs.collapse', '.assigned-repository-container', function() {
var repositoryContainer = $(this);
@ -203,28 +201,61 @@ var MyModuleRepositories = (function() {
});
}
function initVersionsStatusCheck() {
let sidebar = FULL_VIEW_MODAL.find('.repository-versions-sidebar');
sidebar.find('.repository-snapshot-item.provisioning').each(function() {
var snapshotItem = $(this);
setTimeout(function() {
checkSnapshotStatus(snapshotItem);
}, STATUS_POLLING_INTERVAL);
});
}
function initVersionsSidebarActions() {
FULL_VIEW_MODAL.on('click', '#showVersionsSidebar', function(e) {
$.get(FULL_VIEW_MODAL.find('.table').data('versions-sidebar-url'), (data) => {
$.getJSON(FULL_VIEW_MODAL.find('.table').data('versions-sidebar-url'), (data) => {
FULL_VIEW_MODAL.find('.repository-versions-sidebar').html(data.html);
setSelectedItem();
FULL_VIEW_MODAL.find('.table-container').addClass('collapsed');
FULL_VIEW_MODAL.find('.repository-versions-sidebar').removeClass('collapsed');
initVersionsStatusCheck();
});
e.stopPropagation();
});
FULL_VIEW_MODAL.on('click', '#createRepositorySnapshotButton', function(e) {
createDestroySnapshot($(this).data('action-path'), 'POST');
animateSpinner(null, true);
$.ajax({
url: $(this).data('action-path'),
type: 'POST',
dataType: 'json',
success: function(data) {
let snapshotItem = $(data.html);
FULL_VIEW_MODAL.find('.repository-versions-list').append(snapshotItem);
setTimeout(function() {
checkSnapshotStatus(snapshotItem);
}, STATUS_POLLING_INTERVAL);
animateSpinner(null, false);
}
});
e.stopPropagation();
});
FULL_VIEW_MODAL.on('click', '.delete-snapshot-button', function(e) {
let snapshotId = $(this).closest('.repository-snapshot-item').data('id');
createDestroySnapshot($(this).data('action-path'), 'DELETE');
if (snapshotId === FULL_VIEW_MODAL.find('.table').data('id')) {
reloadTable(FULL_VIEW_MODAL.find('#selectLiveVersionButton').data('table-url'));
}
let snapshotItem = $(this).closest('.repository-snapshot-item');
animateSpinner(null, true);
$.ajax({
url: $(this).data('action-path'),
type: 'DELETE',
dataType: 'json',
success: function() {
if (snapshotItem.data('id') === FULL_VIEW_MODAL.find('.table').data('id')) {
reloadTable(FULL_VIEW_MODAL.find('#selectLiveVersionButton').data('table-url'));
}
snapshotItem.remove();
animateSpinner(null, false);
}
});
e.stopPropagation();
});
@ -253,7 +284,7 @@ var MyModuleRepositories = (function() {
FULL_VIEW_MODAL.find('.repository-name').html(repositoryNameObject);
FULL_VIEW_MODAL.modal('show');
$.get($(this).data('table-url'), (data) => {
$.getJSON($(this).data('table-url'), (data) => {
FULL_VIEW_MODAL.find('.table-container').html(data.html);
renderFullViewTable(FULL_VIEW_MODAL.find('.table'));
});
@ -264,7 +295,7 @@ var MyModuleRepositories = (function() {
function initRepositoriesDropdown() {
$('.repositories-assign-container').on('show.bs.dropdown', function() {
var dropdownContainer = $(this);
$.get(dropdownContainer.data('repositories-url'), function(result) {
$.getJSON(dropdownContainer.data('repositories-url'), function(result) {
dropdownContainer.find('.repositories-dropdown-menu').html(result.html);
});
});

View file

@ -30,22 +30,38 @@ class MyModuleRepositorySnapshotsController < ApplicationController
end
def create
service = Repositories::MyModuleAssigningSnapshotService.call(repository: @repository,
my_module: @my_module,
user: current_user)
repository_snapshot = @repository.dup.becomes(RepositorySnapshot)
repository_snapshot.assign_attributes(type: RepositorySnapshot.name,
original_repository: @repository,
my_module: @my_module,
created_by: current_user)
repository_snapshot.provisioning!
repository_snapshot.reload
if service.succeed?
@repository_snapshots = @my_module.repository_snapshots.where(original_repository: @repository)
render json: { html: render_to_string(partial: 'my_modules/repositories/full_view_versions_sidebar') }
else
render json: service.errors, status: :unprocessable_entity
end
RepositorySnapshotProvisioningJob.perform_later(repository_snapshot)
render json: {
html: render_to_string(partial: 'my_modules/repositories/full_view_version',
locals: { repository_snapshot: repository_snapshot })
}
end
def status
render json: {
status: @repository_snapshot.status
}
end
def show
render json: {
html: render_to_string(partial: 'my_modules/repositories/full_view_version',
locals: { repository_snapshot: @repository_snapshot })
}
end
def destroy
@repository_snapshot.destroy!
@repository_snapshots = @my_module.repository_snapshots.where(original_repository: @repository)
render json: { html: render_to_string(partial: 'my_modules/repositories/full_view_versions_sidebar') }
render json: {}
end
def full_view_table

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
retry_on ActiveRecord::Deadlocked
# Most jobs are safe to ignore if the underlying records are no longer available
discard_on ActiveJob::DeserializationError
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class RepositorySnapshotProvisioningJob < ApplicationJob
queue_as :high_priority
def perform(repository_snapshot)
service = Repositories::SnapshotProvisioningService.call(repository_snapshot: repository_snapshot)
repository_snapshot.failed! unless service.succeed?
end
end

View file

@ -1,10 +1,13 @@
# frozen_string_literal: true
class RepositorySnapshot < RepositoryBase
enum status: { provisioning: 0, ready: 1, failed: 2 }
belongs_to :original_repository, foreign_key: :parent_id, class_name: 'Repository', inverse_of: :repository_snapshots
belongs_to :my_module, optional: true
validates :name, presence: true, length: { maximum: Constants::NAME_MAX_LENGTH }
validates :status, presence: true
def default_columns_count
Constants::REPOSITORY_SNAPSHOT_TABLE_DEFAULT_STATE['length']

View file

@ -1,67 +0,0 @@
# frozen_string_literal: true
module Repositories
class MyModuleAssigningSnapshotService
extend Service
attr_reader :repository, :my_module, :user, :errors
def initialize(repository:, my_module:, user:)
@repository = repository
@my_module = my_module
@user = user
@errors = {}
end
def call
return self unless valid?
ActiveRecord::Base.transaction do
repository_snapshot = @repository.dup.becomes(RepositorySnapshot)
repository_snapshot.type = RepositorySnapshot.name
repository_snapshot.original_repository = @repository
repository_snapshot.my_module = @my_module
repository_snapshot.save!
@repository.repository_columns.each do |column|
column.snapshot!(repository_snapshot)
end
repository_rows = @repository.repository_rows
.joins(:my_module_repository_rows)
.where(my_module_repository_rows: { my_module: @my_module })
repository_rows.find_each do |original_row|
original_row.snapshot!(repository_snapshot)
end
rescue ActiveRecord::RecordInvalid => e
@errors[e.record.class.name.underscore] = e.record.errors.full_messages
raise ActiveRecord::Rollback
end
self
end
def succeed?
@errors.none?
end
private
def valid?
unless @repository && @my_module && @user
@errors[:invalid_arguments] =
{ 'repository': @repository,
'my_module': @my_module,
'user': @user }
.map do |key, value|
if value.nil?
I18n.t('repositories.my_module_assigned_snapshot_service.invalid_arguments', key: key.capitalize)
end
end.compact
return false
end
true
end
end
end

View file

@ -0,0 +1,62 @@
# frozen_string_literal: true
module Repositories
class SnapshotProvisioningService
extend Service
attr_reader :repository_snapshot, :errors
def initialize(repository_snapshot:)
@repository_snapshot = repository_snapshot
@errors = {}
end
def call
return self unless valid?
ActiveRecord::Base.transaction do
repository = @repository_snapshot.original_repository
repository.repository_columns.each do |column|
column.snapshot!(@repository_snapshot)
end
repository_rows = repository.repository_rows
.joins(:my_module_repository_rows)
.where(my_module_repository_rows: { my_module: @repository_snapshot.my_module })
repository_rows.find_each do |original_row|
original_row.snapshot!(@repository_snapshot)
end
@repository_snapshot.ready!
rescue ActiveRecord::RecordInvalid => e
@errors[e.record.class.name.underscore] = e.record.errors.full_messages
Rails.logger.error e.message
raise ActiveRecord::Rollback
end
self
end
def succeed?
@errors.none?
end
private
def valid?
unless @repository_snapshot
@errors[:invalid_arguments] =
{ 'repository_snapshot': @repository_snapshot }
.map do |key, value|
if value.nil?
I18n.t('repositories.my_module_assigned_snapshot_service.invalid_arguments', key: key.capitalize)
end
end.compact
return false
end
true
end
end
end

View file

@ -0,0 +1,34 @@
<div class="list-group-item repository-snapshot-item <%= repository_snapshot.status %>" data-id="<%= repository_snapshot.id %>"
data-status-url="<%= status_my_module_repository_snapshot_path(@my_module, @repository, repository_snapshot) %>"
data-item-url="<%= my_module_repository_snapshot_path(@my_module, @repository, repository_snapshot) %>">
<div class="row">
<div class="col-sm-6">
<% if repository_snapshot.status == 'provisioning' %>
<p class="list-group-item-heading">
<i class="fas fa-spinner fa-spin"></i>
<%= t('my_modules.repository.snapshots.full_view.provisioning') %>
</p>
<p class="list-group-item-text">
<%= t('my_modules.repository.snapshots.full_view.created_by', full_name: repository_snapshot.created_by.full_name) %>
</p>
<% else %>
<a class="version-button select-snapshot-button <%= 'disabled' if repository_snapshot.status == 'provisioning' %>"
href="#"
data-status="<%= repository_snapshot.status %>"
data-table-url="<%= full_view_table_my_module_repository_snapshot_path(@my_module, @repository, repository_snapshot) %>">
<h4 class="list-group-item-heading">
<%= l(repository_snapshot.updated_at, format: :full) %>
</h4>
<p class="list-group-item-text">
<%= t('my_modules.repository.snapshots.full_view.created_by', full_name: repository_snapshot.created_by.full_name) %>
</p>
</a>
<% end %>
</div>
<div class="col-sm-6">
<a class="pull-right btn version-button delete-snapshot-button" href="#" data-action-path="<%= my_module_repository_snapshot_path(@my_module, @repository, repository_snapshot) %>">
<i class="fas fa-trash"></i>
</a>
</div>
</div>
</div>

View file

@ -5,7 +5,7 @@
</a>
</h4>
</div>
<div class="list-group">
<div class="list-group repository-versions-list">
<div class="list-group-item live-version-item" data-id="<%= @repository.id %>">
<a id="selectLiveVersionButton" class="version-button" href="#" data-table-url="<%= full_view_table_my_module_repository_path(@my_module, @repository) %>">
<h2 class="list-group-item-heading">
@ -30,25 +30,5 @@
</button>
</div>
<% @repository_snapshots.each do |repository_snapshot| %>
<div class="list-group-item repository-snapshot-item" data-id="<%= repository_snapshot.id %>">
<div class="row">
<div class="col-sm-10">
<a class="version-button select-snapshot-button" href="#" data-table-url="<%= full_view_table_my_module_repository_snapshot_path(@my_module, @repository, repository_snapshot) %>">
<h4 class="list-group-item-heading">
<%= l(repository_snapshot.created_at, format: :full) %>
</h4>
<p class="list-group-item-text">
<%= t('my_modules.repository.snapshots.full_view.created_by', full_name: repository_snapshot.created_by.full_name) %>
</p>
</a>
</div>
<div class="col-sm-2">
<a class="pull-right btn version-button delete-snapshot-button" href="#" data-action-path="<%= my_module_repository_snapshot_path(@my_module, @repository, repository_snapshot) %>">
<i class="fas fa-trash"></i>
</a>
</div>
</div>
</div>
<% end %>
<%= render partial: 'my_modules/repositories/full_view_version', collection: @repository_snapshots, as: :repository_snapshot %>
</div>

View file

@ -63,3 +63,7 @@ Delayed::Worker.max_attempts = DelayedWorkerConfig.max_attempts
Delayed::Worker.max_run_time = DelayedWorkerConfig.max_run_time
Delayed::Worker.read_ahead = DelayedWorkerConfig.read_ahead
Delayed::Worker.default_queue_name = DelayedWorkerConfig.default_queue_name
Delayed::Worker.queue_attributes = {
high_priority: { priority: -10 },
low_priority: { priority: 10 }
}

View file

@ -808,6 +808,7 @@ en:
versions_sidebar_button: 'View versions'
create_button: 'Create snapshot'
created_by: 'by %{full_name}'
provisioning: 'Provisioning'
modals:
assign_repository_record:
title: 'Assign %{repository_name} items to task %{my_module_name}'

View file

@ -394,10 +394,11 @@ Rails.application.routes.draw do
resources :repository_snapshots, controller: 'my_module_repository_snapshots',
as: :snapshots,
only: %i(create destroy) do
only: %i(create show destroy) do
member do
get :full_view_table, to: 'my_module_repository_snapshots#full_view_table'
post :index_dt, to: 'my_module_repository_snapshots#index_dt'
get :status, to: 'my_module_repository_snapshots#status'
end
end

View file

@ -2,9 +2,13 @@
class AddRepositorySnapshots < ActiveRecord::Migration[6.0]
def up
add_column :repositories, :parent_id, :bigint, null: true
change_table :repositories, bulk: true do |t|
t.string :type
t.bigint :parent_id, null: true
t.integer :status, null: true
end
add_reference :repositories, :my_module
add_column :repositories, :type, :string
execute "UPDATE \"repositories\" SET \"type\" = 'Repository'"
execute "UPDATE \"activities\" SET \"subject_type\" = 'RepositoryBase' WHERE \"subject_type\" = 'Repository'"

View file

@ -1100,9 +1100,10 @@ CREATE TABLE public.repositories (
updated_at timestamp without time zone,
discarded_at timestamp without time zone,
permission_level integer DEFAULT 0 NOT NULL,
type character varying,
parent_id bigint,
my_module_id bigint,
type character varying
status integer,
my_module_id bigint
);