Commit 3d94b504 authored by Alex Kalderimis's avatar Alex Kalderimis Committed by Matthias Käppler

Move write operations to a separate worker

See merge request gitlab-org/gitlab!62761
parent 2f97b0fd
...@@ -29,15 +29,17 @@ class WebHookService ...@@ -29,15 +29,17 @@ class WebHookService
GITLAB_EVENT_HEADER = 'X-Gitlab-Event' GITLAB_EVENT_HEADER = 'X-Gitlab-Event'
attr_accessor :hook, :data, :hook_name, :request_options attr_accessor :hook, :data, :hook_name, :request_options
attr_reader :uniqueness_token
def self.hook_to_event(hook_name) def self.hook_to_event(hook_name)
hook_name.to_s.singularize.titleize hook_name.to_s.singularize.titleize
end end
def initialize(hook, data, hook_name) def initialize(hook, data, hook_name, uniqueness_token = nil)
@hook = hook @hook = hook
@data = data @data = data
@hook_name = hook_name.to_s @hook_name = hook_name.to_s
@uniqueness_token = uniqueness_token
@request_options = { @request_options = {
timeout: Gitlab.config.gitlab.webhook_timeout, timeout: Gitlab.config.gitlab.webhook_timeout,
allow_local_requests: hook.allow_local_requests? allow_local_requests: hook.allow_local_requests?
...@@ -123,10 +125,8 @@ class WebHookService ...@@ -123,10 +125,8 @@ class WebHookService
end end
def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil) def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil)
handle_failure(response, hook) category = response_category(response)
log_data = {
WebHookLog.create(
web_hook: hook,
trigger: trigger, trigger: trigger,
url: url, url: url,
execution_duration: execution_duration, execution_duration: execution_duration,
...@@ -136,16 +136,19 @@ class WebHookService ...@@ -136,16 +136,19 @@ class WebHookService
response_body: safe_response_body(response), response_body: safe_response_body(response),
response_status: response.code, response_status: response.code,
internal_error_message: error_message internal_error_message: error_message
) }
::WebHooks::LogExecutionWorker
.perform_async(hook.id, log_data, category, uniqueness_token)
end end
def handle_failure(response, hook) def response_category(response)
if response.success? || response.redirection? if response.success? || response.redirection?
hook.enable! :ok
elsif response.internal_server_error? elsif response.internal_server_error?
hook.backoff! :error
else else
hook.failed! :failed
end end
end end
......
# frozen_string_literal: true
module WebHooks
class LogExecutionService
attr_reader :hook, :log_data, :response_category
def initialize(hook:, log_data:, response_category:)
@hook = hook
@log_data = log_data
@response_category = response_category
end
def execute
update_hook_executability
log_execution
end
private
def log_execution
WebHookLog.create!(web_hook: hook, **log_data.transform_keys(&:to_sym))
end
def update_hook_executability
case response_category
when :ok
hook.enable!
when :error
hook.backoff!
when :failed
hook.failed!
end
end
end
end
...@@ -2828,6 +2828,15 @@ ...@@ -2828,6 +2828,15 @@
:idempotent: true :idempotent: true
:tags: :tags:
- :exclude_from_kubernetes - :exclude_from_kubernetes
- :name: web_hooks_log_execution
:worker_name: WebHooks::LogExecutionWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: wikis_git_garbage_collect - :name: wikis_git_garbage_collect
:worker_name: Wikis::GitGarbageCollectWorker :worker_name: Wikis::GitGarbageCollectWorker
:feature_category: :gitaly :feature_category: :gitaly
......
...@@ -16,7 +16,7 @@ class WebHookWorker ...@@ -16,7 +16,7 @@ class WebHookWorker
hook = WebHook.find(hook_id) hook = WebHook.find(hook_id)
data = data.with_indifferent_access data = data.with_indifferent_access
WebHookService.new(hook, data, hook_name).execute WebHookService.new(hook, data, hook_name, jid).execute
end end
end end
# rubocop:enable Scalability/IdempotentWorker # rubocop:enable Scalability/IdempotentWorker
# frozen_string_literal: true
module WebHooks
class LogExecutionWorker
include ApplicationWorker
idempotent!
feature_category :integrations
urgency :low
# This worker accepts an extra argument. This enables us to
# treat this worker as idempotent. Currently this is set to
# the Job ID (jid) of the parent worker.
def perform(hook_id, log_data, response_category, _unique_by)
hook = WebHook.find_by_id(hook_id)
return unless hook # hook has been deleted before we could run.
::WebHooks::LogExecutionService
.new(hook: hook, log_data: log_data, response_category: response_category.to_sym)
.execute
end
end
end
...@@ -400,6 +400,8 @@ ...@@ -400,6 +400,8 @@
- 1 - 1
- - web_hooks_destroy - - web_hooks_destroy
- 1 - 1
- - web_hooks_log_execution
- 1
- - wikis_git_garbage_collect - - wikis_git_garbage_collect
- 1 - 1
- - x509_certificate_revoke - - x509_certificate_revoke
......
...@@ -174,13 +174,19 @@ RSpec.describe WebHookService do ...@@ -174,13 +174,19 @@ RSpec.describe WebHookService do
context 'execution logging' do context 'execution logging' do
let(:hook_log) { project_hook.web_hook_logs.last } let(:hook_log) { project_hook.web_hook_logs.last }
def run_service
service_instance.execute
::WebHooks::LogExecutionWorker.drain
project_hook.reload
end
context 'with success' do context 'with success' do
before do before do
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success') stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success')
end end
it 'log successful execution' do it 'log successful execution' do
service_instance.execute run_service
expect(hook_log.trigger).to eq('push_hooks') expect(hook_log.trigger).to eq('push_hooks')
expect(hook_log.url).to eq(project_hook.url) expect(hook_log.url).to eq(project_hook.url)
...@@ -191,12 +197,16 @@ RSpec.describe WebHookService do ...@@ -191,12 +197,16 @@ RSpec.describe WebHookService do
expect(hook_log.internal_error_message).to be_nil expect(hook_log.internal_error_message).to be_nil
end end
it 'does not log in the service itself' do
expect { service_instance.execute }.not_to change(::WebHookLog, :count)
end
it 'does not increment the failure count' do it 'does not increment the failure count' do
expect { service_instance.execute }.not_to change(project_hook, :recent_failures) expect { run_service }.not_to change(project_hook, :recent_failures)
end end
it 'does not change the disabled_until attribute' do it 'does not change the disabled_until attribute' do
expect { service_instance.execute }.not_to change(project_hook, :disabled_until) expect { run_service }.not_to change(project_hook, :disabled_until)
end end
context 'when the hook had previously failed' do context 'when the hook had previously failed' do
...@@ -205,7 +215,7 @@ RSpec.describe WebHookService do ...@@ -205,7 +215,7 @@ RSpec.describe WebHookService do
end end
it 'resets the failure count' do it 'resets the failure count' do
expect { service_instance.execute }.to change(project_hook, :recent_failures).to(0) expect { run_service }.to change(project_hook, :recent_failures).to(0)
end end
end end
end end
...@@ -216,7 +226,7 @@ RSpec.describe WebHookService do ...@@ -216,7 +226,7 @@ RSpec.describe WebHookService do
end end
it 'logs failed execution' do it 'logs failed execution' do
service_instance.execute run_service
expect(hook_log).to have_attributes( expect(hook_log).to have_attributes(
trigger: eq('push_hooks'), trigger: eq('push_hooks'),
...@@ -230,17 +240,17 @@ RSpec.describe WebHookService do ...@@ -230,17 +240,17 @@ RSpec.describe WebHookService do
end end
it 'increments the failure count' do it 'increments the failure count' do
expect { service_instance.execute }.to change(project_hook, :recent_failures).by(1) expect { run_service }.to change(project_hook, :recent_failures).by(1)
end end
it 'does not change the disabled_until attribute' do it 'does not change the disabled_until attribute' do
expect { service_instance.execute }.not_to change(project_hook, :disabled_until) expect { run_service }.not_to change(project_hook, :disabled_until)
end end
it 'does not allow the failure count to overflow' do it 'does not allow the failure count to overflow' do
project_hook.update!(recent_failures: 32767) project_hook.update!(recent_failures: 32767)
expect { service_instance.execute }.not_to change(project_hook, :recent_failures) expect { run_service }.not_to change(project_hook, :recent_failures)
end end
context 'when the web_hooks_disable_failed FF is disabled' do context 'when the web_hooks_disable_failed FF is disabled' do
...@@ -252,7 +262,7 @@ RSpec.describe WebHookService do ...@@ -252,7 +262,7 @@ RSpec.describe WebHookService do
it 'does not allow the failure count to overflow' do it 'does not allow the failure count to overflow' do
project_hook.update!(recent_failures: 32767) project_hook.update!(recent_failures: 32767)
expect { service_instance.execute }.not_to change(project_hook, :recent_failures) expect { run_service }.not_to change(project_hook, :recent_failures)
end end
end end
end end
...@@ -263,7 +273,7 @@ RSpec.describe WebHookService do ...@@ -263,7 +273,7 @@ RSpec.describe WebHookService do
end end
it 'log failed execution' do it 'log failed execution' do
service_instance.execute run_service
expect(hook_log.trigger).to eq('push_hooks') expect(hook_log.trigger).to eq('push_hooks')
expect(hook_log.url).to eq(project_hook.url) expect(hook_log.url).to eq(project_hook.url)
...@@ -275,17 +285,15 @@ RSpec.describe WebHookService do ...@@ -275,17 +285,15 @@ RSpec.describe WebHookService do
end end
it 'does not increment the failure count' do it 'does not increment the failure count' do
expect { service_instance.execute }.not_to change(project_hook, :recent_failures) expect { run_service }.not_to change(project_hook, :recent_failures)
end end
it 'backs off' do it 'backs off' do
expect(project_hook).to receive(:backoff!).and_call_original expect { run_service }.to change(project_hook, :disabled_until)
expect { service_instance.execute }.to change(project_hook, :disabled_until)
end end
it 'increases the backoff count' do it 'increases the backoff count' do
expect { service_instance.execute }.to change(project_hook, :backoff_count).by(1) expect { run_service }.to change(project_hook, :backoff_count).by(1)
end end
context 'when the previous cool-off was near the maximum' do context 'when the previous cool-off was near the maximum' do
...@@ -294,7 +302,7 @@ RSpec.describe WebHookService do ...@@ -294,7 +302,7 @@ RSpec.describe WebHookService do
end end
it 'sets the disabled_until attribute' do it 'sets the disabled_until attribute' do
expect { service_instance.execute }.to change(project_hook, :disabled_until).to(1.day.from_now) expect { run_service }.to change(project_hook, :disabled_until).to(1.day.from_now)
end end
end end
...@@ -304,7 +312,7 @@ RSpec.describe WebHookService do ...@@ -304,7 +312,7 @@ RSpec.describe WebHookService do
end end
it 'sets the disabled_until attribute' do it 'sets the disabled_until attribute' do
expect { service_instance.execute }.to change(project_hook, :disabled_until).to(1.day.from_now) expect { run_service }.to change(project_hook, :disabled_until).to(1.day.from_now)
end end
end end
end end
...@@ -312,7 +320,7 @@ RSpec.describe WebHookService do ...@@ -312,7 +320,7 @@ RSpec.describe WebHookService do
context 'with unsafe response body' do context 'with unsafe response body' do
before do before do
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: "\xBB") stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: "\xBB")
service_instance.execute run_service
end end
it 'log successful execution' do it 'log successful execution' do
......
...@@ -10,7 +10,7 @@ RSpec.describe WebHookWorker do ...@@ -10,7 +10,7 @@ RSpec.describe WebHookWorker do
describe '#perform' do describe '#perform' do
it 'delegates to WebHookService' do it 'delegates to WebHookService' do
expect_next(WebHookService, project_hook, data.with_indifferent_access, hook_name).to receive(:execute) expect_next(WebHookService, project_hook, data.with_indifferent_access, hook_name, anything).to receive(:execute)
subject.perform(project_hook.id, data, hook_name) subject.perform(project_hook.id, data, hook_name)
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