diff options
Diffstat (limited to 'app/models/hooks')
-rw-r--r-- | app/models/hooks/project_hook.rb | 12 | ||||
-rw-r--r-- | app/models/hooks/service_hook.rb | 8 | ||||
-rw-r--r-- | app/models/hooks/web_hook.rb | 48 | ||||
-rw-r--r-- | app/models/hooks/web_hook_log.rb | 3 | ||||
-rw-r--r-- | app/models/hooks/web_hook_log_archived.rb | 12 | ||||
-rw-r--r-- | app/models/hooks/web_hook_log_partitioned.rb | 17 |
6 files changed, 75 insertions, 25 deletions
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index b625a70b444..a28b97e63e5 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -4,6 +4,7 @@ class ProjectHook < WebHook include TriggerableHooks include Presentable include Limitable + extend ::Gitlab::Utils::Override self.limit_scope = :project @@ -29,6 +30,15 @@ class ProjectHook < WebHook def pluralized_name _('Webhooks') end + + def web_hooks_disable_failed? + Feature.enabled?(:web_hooks_disable_failed, project) + end + + override :rate_limit + def rate_limit + project.actual_limits.limit_for(:web_hook_calls) + end end -ProjectHook.prepend_if_ee('EE::ProjectHook') +ProjectHook.prepend_mod_with('ProjectHook') diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index 4caa45a13d4..1a466b333a5 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -3,12 +3,10 @@ class ServiceHook < WebHook include Presentable - belongs_to :service - validates :service, presence: true + belongs_to :integration, foreign_key: :service_id + validates :integration, presence: true - # rubocop: disable CodeReuse/ServiceClass def execute(data, hook_name = 'service_hook') - WebHookService.new(self, data, hook_name).execute + super(data, hook_name) end - # rubocop: enable CodeReuse/ServiceClass end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index dbd5a1b032a..02b4feb4ccc 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -3,6 +3,11 @@ class WebHook < ApplicationRecord include Sortable + FAILURE_THRESHOLD = 3 # three strikes + INITIAL_BACKOFF = 10.minutes + MAX_BACKOFF = 1.day + BACKOFF_GROWTH_FACTOR = 2.0 + attr_encrypted :token, mode: :per_attribute_iv, algorithm: 'aes-256-gcm', @@ -21,15 +26,27 @@ class WebHook < ApplicationRecord validates :token, format: { without: /\n/ } validates :push_events_branch_filter, branch_filter: true + scope :executable, -> do + next all unless Feature.enabled?(:web_hooks_disable_failed) + + where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current) + end + + def executable? + return true unless web_hooks_disable_failed? + + recent_failures <= FAILURE_THRESHOLD && (disabled_until.nil? || disabled_until < Time.current) + end + # rubocop: disable CodeReuse/ServiceClass def execute(data, hook_name) - WebHookService.new(self, data, hook_name).execute + WebHookService.new(self, data, hook_name).execute if executable? end # rubocop: enable CodeReuse/ServiceClass # rubocop: disable CodeReuse/ServiceClass def async_execute(data, hook_name) - WebHookService.new(self, data, hook_name).async_execute + WebHookService.new(self, data, hook_name).async_execute if executable? end # rubocop: enable CodeReuse/ServiceClass @@ -41,4 +58,31 @@ class WebHook < ApplicationRecord def help_path 'user/project/integrations/webhooks' end + + def next_backoff + return MAX_BACKOFF if backoff_count >= 8 # optimization to prevent expensive exponentiation and possible overflows + + (INITIAL_BACKOFF * (BACKOFF_GROWTH_FACTOR**backoff_count)) + .clamp(INITIAL_BACKOFF, MAX_BACKOFF) + .seconds + end + + def disable! + update!(recent_failures: FAILURE_THRESHOLD + 1) + end + + def enable! + update!(recent_failures: 0, disabled_until: nil, backoff_count: 0) + end + + # Overridden in ProjectHook and GroupHook, other webhooks are not rate-limited. + def rate_limit + nil + end + + private + + def web_hooks_disable_failed? + Feature.enabled?(:web_hooks_disable_failed) + end end diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb index e2230a2d644..0c96d5d4b6d 100644 --- a/app/models/hooks/web_hook_log.rb +++ b/app/models/hooks/web_hook_log.rb @@ -5,9 +5,12 @@ class WebHookLog < ApplicationRecord include Presentable include DeleteWithLimit include CreatedAtFilterable + include PartitionedTable self.primary_key = :id + partitioned_by :created_at, strategy: :monthly + belongs_to :web_hook serialize :request_headers, Hash # rubocop:disable Cop/ActiveRecordSerialize diff --git a/app/models/hooks/web_hook_log_archived.rb b/app/models/hooks/web_hook_log_archived.rb new file mode 100644 index 00000000000..a1c8a44f5ba --- /dev/null +++ b/app/models/hooks/web_hook_log_archived.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# This model is not intended to be used. +# It is a temporary reference to the old non-partitioned +# web_hook_logs table. +# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/5558 +# for details. +# rubocop:disable Gitlab/NamespacedClass: This is a temporary class with no relevant namespace +# WebHook, WebHookLog and all hooks are defined outside of a namespace +class WebHookLogArchived < ApplicationRecord + self.table_name = 'web_hook_logs_archived' +end diff --git a/app/models/hooks/web_hook_log_partitioned.rb b/app/models/hooks/web_hook_log_partitioned.rb deleted file mode 100644 index b4b150afb6a..00000000000 --- a/app/models/hooks/web_hook_log_partitioned.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -# This model is not yet intended to be used. -# It is in a transitioning phase while we are partitioning -# the web_hook_logs table on the database-side. -# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/5558 -# for details. -# rubocop:disable Gitlab/NamespacedClass: This is a temporary class with no relevant namespace -# WebHook, WebHookLog and all hooks are defined outside of a namespace -class WebHookLogPartitioned < ApplicationRecord - include PartitionedTable - - self.table_name = 'web_hook_logs_part_0c5294f417' - self.primary_key = :id - - partitioned_by :created_at, strategy: :monthly -end |