Commit 14d5ae82 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'pl-status-page-mvc-background-job' into 'master'

Add background worker to publish incident to status page

See merge request gitlab-org/gitlab!26717
parents 95e0f2d3 4ce65778
...@@ -13,6 +13,17 @@ module ApplicationWorker ...@@ -13,6 +13,17 @@ module ApplicationWorker
included do included do
set_queue set_queue
def structured_payload(payload = {})
context = Labkit::Context.current.to_h.merge(
'class' => self.class,
'job_status' => 'running',
'queue' => self.class.queue,
'jid' => jid
)
payload.stringify_keys.merge(context)
end
end end
class_methods do class_methods do
......
...@@ -232,6 +232,8 @@ ...@@ -232,6 +232,8 @@
- 2 - 2
- - service_desk_email_receiver - - service_desk_email_receiver
- 1 - 1
- - status_page_publish_incident
- 1
- - sync_seat_link_request - - sync_seat_link_request
- 1 - 1
- - system_hook_push - - system_hook_push
......
...@@ -124,6 +124,11 @@ module EE ...@@ -124,6 +124,11 @@ module EE
@subject.feature_available?(:code_review_analytics, @user) @subject.feature_available?(:code_review_analytics, @user)
end end
condition(:status_page_available) do
@subject.feature_available?(:status_page, @user) &&
@subject.beta_feature_available?(:status_page)
end
condition(:group_timelogs_available) do condition(:group_timelogs_available) do
@subject.feature_available?(:group_timelogs) @subject.feature_available?(:group_timelogs)
end end
...@@ -366,6 +371,8 @@ module EE ...@@ -366,6 +371,8 @@ module EE
end end
rule { requirements_available & owner }.enable :destroy_requirement rule { requirements_available & owner }.enable :destroy_requirement
rule { status_page_available & can?(:developer_access) }.enable :publish_status_page
end end
override :lookup_access_level! override :lookup_access_level!
......
# frozen_string_literal: true # frozen_string_literal: true
module StatusPage module StatusPage
# Delegate work to more specific publishing services. # Publishes content to status page by delegating to specific
# publishing services.
# #
# Use this service for publishing an incident to CDN which calls: # Use this service for publishing an incident to CDN synchronously.
# To publish asynchronously use +StatusPage::TriggerPublishService+ instead.
#
# This services calls:
# * StatusPage::PublishDetailsService # * StatusPage::PublishDetailsService
# * StatusPage::PublishListService # * StatusPage::PublishListService
class PublishIncidentService class PublishIncidentService
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
def initialize(project:, issue_id:) def initialize(user:, project:, issue_id:)
@user = user
@project = project @project = project
@issue_id = issue_id @issue_id = issue_id
end end
def execute def execute
return error_permission_denied unless can_publish?
return error_issue_not_found unless issue return error_issue_not_found unless issue
response = publish_details response = publish_details
...@@ -25,7 +31,7 @@ module StatusPage ...@@ -25,7 +31,7 @@ module StatusPage
private private
attr_reader :project, :issue_id attr_reader :user, :project, :issue_id
def publish_details def publish_details
PublishDetailsService.new(project: project).execute(issue, user_notes) PublishDetailsService.new(project: project).execute(issue, user_notes)
...@@ -55,8 +61,20 @@ module StatusPage ...@@ -55,8 +61,20 @@ module StatusPage
end end
end end
def can_publish?
user.can?(:publish_status_page, project)
end
def error_permission_denied
error('No publish permission')
end
def error_issue_not_found def error_issue_not_found
ServiceResponse.error(message: 'Issue not found') error('Issue not found')
end
def error(message)
ServiceResponse.error(message: message)
end end
end end
end end
# frozen_string_literal: true
module StatusPage
# Triggers a background job to publish of incidents to the status page.
#
# Use this service when issues/notes/emoji have changed to kickoff the
# publish process.
class TriggerPublishService
def initialize(user:, project:)
@user = user
@project = project
end
def execute(issue_id)
return unless can_publish?
return unless status_page_enabled?
StatusPage::PublishIncidentWorker
.perform_async(user.id, project.id, issue_id)
end
private
attr_reader :user, :project
def can_publish?
user&.can?(:publish_status_page, project)
end
def status_page_enabled?
project.status_page_setting&.enabled?
end
end
end
...@@ -570,6 +570,13 @@ ...@@ -570,6 +570,13 @@
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
:idempotent: :idempotent:
- :name: status_page_publish_incident
:feature_category: :status_page
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :name: sync_seat_link_request - :name: sync_seat_link_request
:feature_category: :analysis :feature_category: :analysis
:has_external_dependencies: true :has_external_dependencies: true
......
# frozen_string_literal: true
module StatusPage
class PublishIncidentWorker
include ApplicationWorker
include Gitlab::Utils::StrongMemoize
sidekiq_options retry: 5
feature_category :status_page
worker_has_external_dependencies!
idempotent!
def perform(user_id, project_id, issue_id)
@user_id = user_id
@project_id = project_id
@issue_id = issue_id
return unless user && project
publish
end
private
attr_reader :user_id, :project_id, :issue_id
def publish
result = PublishIncidentService
.new(user: user, project: project, issue_id: issue_id)
.execute
log_info(result.message) if result.error?
end
def user
strong_memoize(:user) { User.find_by_id(user_id) }
end
def project
strong_memoize(:project) { Project.find_by_id(project_id) }
end
def log_info(message)
logger.info(structured_payload(message: message))
end
end
end
...@@ -1022,6 +1022,55 @@ describe ProjectPolicy do ...@@ -1022,6 +1022,55 @@ describe ProjectPolicy do
end end
end end
describe 'publish_status_page' do
let(:anonymous) { nil }
let(:feature) { :status_page }
let(:policy) { :publish_status_page }
context 'when feature is available' do
using RSpec::Parameterized::TableSyntax
where(:role, :allowed) do
:anonymous | false
:guest | false
:reporter | false
:developer | true
:maintainer | true
:owner | true
:admin | true
end
with_them do
let(:current_user) { public_send(role) if role }
before do
stub_feature_flags(feature => true)
stub_licensed_features(feature => true)
end
it do
is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy))
end
context 'when feature is not available' do
before do
stub_licensed_features(feature => false)
end
it { is_expected.to be_disallowed(policy) }
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(feature => false)
end
it { is_expected.to be_disallowed(policy) }
end
end
end
end
context 'support bot' do context 'support bot' do
let(:current_user) { User.support_bot } let(:current_user) { User.support_bot }
......
...@@ -3,17 +3,24 @@ ...@@ -3,17 +3,24 @@
require 'spec_helper' require 'spec_helper'
describe StatusPage::PublishIncidentService do describe StatusPage::PublishIncidentService do
let_it_be(:user) { create(:user) }
let_it_be(:project, refind: true) { create(:project) } let_it_be(:project, refind: true) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:settings) { create(:status_page_setting, :enabled, project: project) } let_it_be(:settings) { create(:status_page_setting, :enabled, project: project) }
let(:user_can_publish) { true }
let(:service) { described_class.new(project: project, issue_id: issue.id) } let(:service) do
described_class.new(user: user, project: project, issue_id: issue.id)
end
subject(:result) { service.execute } subject(:result) { service.execute }
describe '#execute' do describe '#execute' do
before do before do
stub_licensed_features(status_page: true) stub_licensed_features(status_page: true)
allow(user).to receive(:can?).with(:publish_status_page, project)
.and_return(user_can_publish)
end end
context 'when publishing succeeds' do context 'when publishing succeeds' do
...@@ -50,6 +57,15 @@ describe StatusPage::PublishIncidentService do ...@@ -50,6 +57,15 @@ describe StatusPage::PublishIncidentService do
expect(result.message).to eq('Issue not found') expect(result.message).to eq('Issue not found')
end end
end end
context 'when user cannot publish' do
let(:user_can_publish) { false }
it 'returns error missing publish permission' do
expect(result).to be_error
expect(result.message).to eq('No publish permission')
end
end
end end
private private
......
# frozen_string_literal: true
require 'spec_helper'
describe StatusPage::TriggerPublishService do
let_it_be(:user) { create(:user) }
let_it_be(:project, refind: true) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let(:service) { described_class.new(user: user, project: project) }
let(:worker) { StatusPage::PublishIncidentWorker }
let_it_be(:status_page_setting) do
create(:status_page_setting, :enabled, project: project)
end
subject { service.execute(issue.id) }
shared_examples 'no job scheduled' do
it 'does not schedule a job' do
expect(worker).not_to receive(:perform_async)
subject
end
end
describe '#execute' do
before do
project.add_maintainer(user)
stub_feature_flags(status_page: true)
stub_licensed_features(status_page: true)
allow(worker).to receive(:perform_async)
.with(user.id, project.id, issue.id)
end
it 'schedules a job' do
expect(worker).to receive(:perform_async)
.with(user.id, project.id, issue.id)
subject
end
context 'when status page is missing' do
before do
status_page_setting.destroy
end
include_examples 'no job scheduled'
end
context 'when status page is not enabled' do
before do
status_page_setting.update!(enabled: false)
end
include_examples 'no job scheduled'
end
context 'when license is not available' do
before do
stub_licensed_features(status_page: false)
end
include_examples 'no job scheduled'
end
context 'when feature is disabled' do
before do
stub_feature_flags(status_page: false)
end
include_examples 'no job scheduled'
end
context 'when user cannot publish status page' do
before do
project.add_reporter(user)
end
include_examples 'no job scheduled'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe StatusPage::PublishIncidentWorker do
include ExclusiveLeaseHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let(:worker) { described_class.new }
let(:logger) { worker.send(:logger) }
let(:service) { instance_double(StatusPage::PublishIncidentService) }
let(:service_result) { ServiceResponse.success }
before do
allow(StatusPage::PublishIncidentService)
.to receive(:new).with(user: user, project: project, issue_id: issue.id)
.and_return(service)
allow(service).to receive(:execute)
.and_return(service_result)
end
describe '#perform' do
subject { worker.perform(user.id, project.id, issue.id) }
it_behaves_like 'an idempotent worker' do
let(:job_args) { [user.id, project.id, issue.id] }
context 'when service succeeds' do
it 'execute the service' do
expect(service).to receive(:execute)
subject
end
end
context 'with unknown project' do
let(:project) { build(:project) }
it 'does not execute the service' do
expect(StatusPage::PublishIncidentService).not_to receive(:execute)
subject
end
end
context 'when service returns an error' do
let(:error_message) { 'some message' }
let(:service_result) { ServiceResponse.error(message: error_message) }
it 'succeeds and logs the error' do
expect(logger)
.to receive(:info)
.with(a_hash_including('message' => error_message))
.exactly(worker_exec_times).times
subject
end
end
end
context 'when service raises an exception' do
let(:error_message) { 'some exception' }
let(:exception) { StandardError.new(error_message) }
it 're-raises exception' do
allow(service).to receive(:execute).and_raise(exception)
expect { subject }.to raise_error(exception)
end
end
end
end
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
# end # end
# #
RSpec.shared_examples 'an idempotent worker' do RSpec.shared_examples 'an idempotent worker' do
let(:worker_exec_times) { IdempotentWorkerHelper::WORKER_EXEC_TIMES }
# Avoid stubbing calls for a more accurate run. # Avoid stubbing calls for a more accurate run.
subject do subject do
defined?(job_args) ? perform_multiple(job_args) : perform_multiple defined?(job_args) ? perform_multiple(job_args) : perform_multiple
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe ApplicationWorker do describe ApplicationWorker do
let(:worker) do let_it_be(:worker) do
Class.new do Class.new do
def self.name def self.name
'Gitlab::Foo::Bar::DummyWorker' 'Gitlab::Foo::Bar::DummyWorker'
...@@ -13,12 +13,51 @@ describe ApplicationWorker do ...@@ -13,12 +13,51 @@ describe ApplicationWorker do
end end
end end
let(:instance) { worker.new }
describe 'Sidekiq options' do describe 'Sidekiq options' do
it 'sets the queue name based on the class name' do it 'sets the queue name based on the class name' do
expect(worker.sidekiq_options['queue']).to eq('foo_bar_dummy') expect(worker.sidekiq_options['queue']).to eq('foo_bar_dummy')
end end
end end
describe '#structured_payload' do
let(:payload) { {} }
subject(:result) { instance.structured_payload(payload) }
it 'adds worker related payload' do
instance.jid = 'a jid'
expect(result).to include(
'class' => worker.class,
'job_status' => 'running',
'queue' => worker.queue,
'jid' => instance.jid
)
end
it 'adds labkit context' do
user = build_stubbed(:user, username: 'jane-doe')
instance.with_context(user: user) do
expect(result).to include('meta.user' => user.username)
end
end
it 'adds custom payload converting stringified keys' do
payload[:message] = 'some message'
expect(result).to include('message' => payload[:message])
end
it 'does not override predefined context keys with custom payload' do
payload['class'] = 'custom value'
expect(result).to include('class' => worker.class)
end
end
describe '.queue_namespace' do describe '.queue_namespace' do
it 'sets the queue name based on the class name' do it 'sets the queue name based on the class name' do
worker.queue_namespace :some_namespace worker.queue_namespace :some_namespace
......
...@@ -21,12 +21,12 @@ describe ExpireJobCacheWorker do ...@@ -21,12 +21,12 @@ describe ExpireJobCacheWorker do
allow(Gitlab::EtagCaching::Store).to receive(:new) { spy_store } allow(Gitlab::EtagCaching::Store).to receive(:new) { spy_store }
expect(spy_store).to receive(:touch) expect(spy_store).to receive(:touch)
.exactly(IdempotentWorkerHelper::WORKER_EXEC_TIMES).times .exactly(worker_exec_times).times
.with(pipeline_path) .with(pipeline_path)
.and_call_original .and_call_original
expect(spy_store).to receive(:touch) expect(spy_store).to receive(:touch)
.exactly(IdempotentWorkerHelper::WORKER_EXEC_TIMES).times .exactly(worker_exec_times).times
.with(job_path) .with(job_path)
.and_call_original .and_call_original
......
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