Commit ebf6b90a authored by charlie ablett's avatar charlie ablett

Merge branch 'add-jira-imports-model' into 'master'

Track jira import state in its own data structure

See merge request gitlab-org/gitlab!28709
parents 827594ab 8952f285
# frozen_string_literal: true
class JiraImportState < ApplicationRecord
include AfterCommitQueue
self.table_name = 'jira_imports'
STATUSES = { initial: 0, scheduled: 1, started: 2, failed: 3, finished: 4 }.freeze
belongs_to :project
belongs_to :user
belongs_to :label
validates :project, presence: true
validates :jira_project_key, presence: true
validates :jira_project_name, presence: true
validates :jira_project_xid, presence: true
validates :project, uniqueness: {
conditions: -> { where.not(status: STATUSES.values_at(:failed, :finished)) },
message: _('Cannot have multiple Jira imports running at the same time')
}
state_machine :status, initial: :initial do
event :schedule do
transition initial: :scheduled
end
event :start do
transition scheduled: :started
end
event :finish do
transition started: :finished
end
event :do_fail do
transition [:initial, :scheduled, :started] => :failed
end
after_transition initial: :scheduled do |state, _|
state.run_after_commit do
job_id = Gitlab::JiraImport::Stage::StartImportWorker.perform_async(project.id)
state.update(jid: job_id) if job_id
end
end
after_transition any => :finished do |state, _|
if state.jid.present?
Gitlab::SidekiqStatus.unset(state.jid)
state.update_column(:jid, nil)
end
end
# Supress warning:
# both JiraImportState and its :status machine have defined a different default for "status".
# although both have same value but represented in 2 ways: integer(0) and symbol(:initial)
def owner_class_attribute_default
'initial'
end
end
enum status: STATUSES
def in_progress?
scheduled? || started?
end
def refresh_jid_expiration
return unless jid
Gitlab::SidekiqStatus.set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
end
def self.jid_by(project_id:, status:)
select(:jid).with_status(status).find_by(project_id: project_id)
end
end
...@@ -314,6 +314,7 @@ class Project < ApplicationRecord ...@@ -314,6 +314,7 @@ class Project < ApplicationRecord
has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project
has_many :import_failures, inverse_of: :project has_many :import_failures, inverse_of: :project
has_many :jira_imports, -> { order 'jira_imports.created_at' }, class_name: 'JiraImportState', inverse_of: :project
has_many :daily_report_results, class_name: 'Ci::DailyReportResult' has_many :daily_report_results, class_name: 'Ci::DailyReportResult'
...@@ -2424,6 +2425,10 @@ class Project < ApplicationRecord ...@@ -2424,6 +2425,10 @@ class Project < ApplicationRecord
environments.where("name LIKE (#{::Gitlab::SQL::Glob.to_like(quoted_scope)})") # rubocop:disable GitlabSecurity/SqlInjection environments.where("name LIKE (#{::Gitlab::SQL::Glob.to_like(quoted_scope)})") # rubocop:disable GitlabSecurity/SqlInjection
end end
def latest_jira_import
jira_imports.last
end
private private
def find_service(services, name) def find_service(services, name)
......
...@@ -3353,6 +3353,9 @@ msgstr "" ...@@ -3353,6 +3353,9 @@ msgstr ""
msgid "Cannot create the abuse report. This user has been blocked." msgid "Cannot create the abuse report. This user has been blocked."
msgstr "" msgstr ""
msgid "Cannot have multiple Jira imports running at the same time"
msgstr ""
msgid "Cannot make epic confidential if it contains not-confidential issues" msgid "Cannot make epic confidential if it contains not-confidential issues"
msgstr "" msgstr ""
......
# frozen_string_literal: true
FactoryBot.define do
factory :jira_import_state do
project
user { project&.creator }
label
jira_project_name { generate(:name) }
jira_project_key { generate(:name) }
jira_project_xid { 1234 }
end
trait :scheduled do
status { :scheduled }
end
trait :started do
status { :started }
end
trait :failed do
status { :failed }
end
trait :finished do
status { :finished }
end
end
...@@ -476,6 +476,7 @@ project: ...@@ -476,6 +476,7 @@ project:
- requirements - requirements
- export_jobs - export_jobs
- daily_report_results - daily_report_results
- jira_imports
award_emoji: award_emoji:
- awardable - awardable
- user - user
......
# frozen_string_literal: true
require 'spec_helper'
describe JiraImportState do
describe "associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:label) }
end
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(AfterCommitQueue) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:jira_project_key) }
it { is_expected.to validate_presence_of(:jira_project_name) }
it { is_expected.to validate_presence_of(:jira_project_xid) }
context 'when trying to run multiple imports' do
let(:project) { create(:project) }
context 'when project has an initial jira_import' do
let!(:jira_import) { create(:jira_import_state, project: project)}
it_behaves_like 'multiple running imports not allowed'
end
context 'when project has a scheduled jira_import' do
let!(:jira_import) { create(:jira_import_state, :scheduled, project: project)}
it_behaves_like 'multiple running imports not allowed'
end
context 'when project has a started jira_import' do
let!(:jira_import) { create(:jira_import_state, :started, project: project)}
it_behaves_like 'multiple running imports not allowed'
end
context 'when project has a failed jira_import' do
let!(:jira_import) { create(:jira_import_state, :failed, project: project)}
it 'returns valid' do
new_import = build(:jira_import_state, project: project)
expect(new_import).to be_valid
expect(new_import.errors[:project]).to be_empty
end
end
context 'when project has a finished jira_import' do
let!(:jira_import) { create(:jira_import_state, :finished, project: project)}
it 'returns valid' do
new_import = build(:jira_import_state, project: project)
expect(new_import).to be_valid
expect(new_import.errors[:project]).to be_empty
end
end
end
end
describe '#in_progress?' do
context 'statuses that return in progress' do
it_behaves_like 'in progress', :scheduled
it_behaves_like 'in progress', :started
end
context 'statuses that return not in progress' do
it_behaves_like 'not in progress', :initial
it_behaves_like 'not in progress', :failed
it_behaves_like 'not in progress', :finished
end
end
describe 'states transition flow' do
let(:project) { create(:project) }
context 'when jira import is in initial state' do
let!(:jira_import) { build(:jira_import_state, project: project)}
it_behaves_like 'can transition', [:schedule, :do_fail]
it_behaves_like 'cannot transition', [:start, :finish]
end
context 'when jira import is in scheduled state' do
let!(:jira_import) { build(:jira_import_state, :scheduled, project: project)}
it_behaves_like 'can transition', [:start, :do_fail]
it_behaves_like 'cannot transition', [:finish]
end
context 'when jira import is in started state' do
let!(:jira_import) { build(:jira_import_state, :started, project: project)}
it_behaves_like 'can transition', [:finish, :do_fail]
it_behaves_like 'cannot transition', [:schedule]
end
context 'when jira import is in failed state' do
let!(:jira_import) { build(:jira_import_state, :failed, project: project)}
it_behaves_like 'cannot transition', [:schedule, :finish, :do_fail]
end
context 'when jira import is in finished state' do
let!(:jira_import) { build(:jira_import_state, :finished, project: project)}
it_behaves_like 'cannot transition', [:schedule, :do_fail, :start]
end
context 'after transition to scheduled' do
let!(:jira_import) { build(:jira_import_state, project: project)}
it 'triggers the import job' do
expect(Gitlab::JiraImport::Stage::StartImportWorker).to receive(:perform_async).and_return('some-job-id')
jira_import.schedule
expect(jira_import.jid).to eq('some-job-id')
end
end
context 'after transition to finished' do
let!(:jira_import) { build(:jira_import_state, :started, jid: 'some-other-jid', project: project)}
it 'triggers the import job' do
jira_import.finish
expect(jira_import.jid).to be_nil
end
it 'triggers the import job' do
jira_import.update!(status: :scheduled)
jira_import.finish
expect(jira_import.status).to eq('scheduled')
expect(jira_import.jid).to eq('some-other-jid')
end
end
end
end
...@@ -110,6 +110,7 @@ describe Project do ...@@ -110,6 +110,7 @@ describe Project do
it { is_expected.to have_many(:source_pipelines) } it { is_expected.to have_many(:source_pipelines) }
it { is_expected.to have_many(:prometheus_alert_events) } it { is_expected.to have_many(:prometheus_alert_events) }
it { is_expected.to have_many(:self_managed_prometheus_alert_events) } it { is_expected.to have_many(:self_managed_prometheus_alert_events) }
it { is_expected.to have_many(:jira_imports) }
it_behaves_like 'model with repository' do it_behaves_like 'model with repository' do
let_it_be(:container) { create(:project, :repository, path: 'somewhere') } let_it_be(:container) { create(:project, :repository, path: 'somewhere') }
...@@ -5987,6 +5988,34 @@ describe Project do ...@@ -5987,6 +5988,34 @@ describe Project do
end end
end end
describe '#latest_jira_import' do
let_it_be(:project) { create(:project) }
context 'when no jira imports' do
it 'returns nil' do
expect(project.latest_jira_import).to be nil
end
end
context 'when single jira import' do
let!(:jira_import1) { create(:jira_import_state, project: project) }
it 'returns the jira import' do
expect(project.latest_jira_import).to eq(jira_import1)
end
end
context 'when multiple jira imports' do
let!(:jira_import1) { create(:jira_import_state, :finished, created_at: 1.day.ago, project: project) }
let!(:jira_import2) { create(:jira_import_state, :failed, created_at: 2.days.ago, project: project) }
let!(:jira_import3) { create(:jira_import_state, :started, created_at: 3.days.ago, project: project) }
it 'returns latest jira import by created_at' do
expect(project.jira_imports.pluck(:id)).to eq([jira_import3.id, jira_import2.id, jira_import1.id])
expect(project.latest_jira_import).to eq(jira_import1)
end
end
end
def finish_job(export_job) def finish_job(export_job)
export_job.start export_job.start
export_job.finish export_job.finish
......
# frozen_string_literal: true
shared_examples 'multiple running imports not allowed' do
it 'returns not valid' do
new_import = build(:jira_import_state, project: project)
expect(new_import).not_to be_valid
expect(new_import.errors[:project]).not_to be_nil
end
end
shared_examples 'in progress' do |status|
it 'returns true' do
jira_import_state = build(:jira_import_state, status: status)
expect(jira_import_state).to be_in_progress
end
end
shared_examples 'not in progress' do |status|
it 'returns false' do
jira_import_state = build(:jira_import_state, status: status)
expect(jira_import_state).not_to be_in_progress
end
end
shared_examples 'can transition' do |states|
states.each do |state|
it 'returns true' do
expect(jira_import.send(state)).to be true
end
end
end
shared_examples 'cannot transition' do |states|
states.each do |state|
it 'returns false' do
expect(jira_import.send(state)).to be false
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