diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 11:17:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 11:17:02 +0300 |
commit | b39512ed755239198a9c294b6a45e65c05900235 (patch) | |
tree | d234a3efade1de67c46b9e5a38ce813627726aa7 /lib/gitlab/background_task.rb | |
parent | d31474cf3b17ece37939d20082b07f6657cc79a9 (diff) |
Add latest changes from gitlab-org/gitlab@15-3-stable-eev15.3.0-rc42
Diffstat (limited to 'lib/gitlab/background_task.rb')
-rw-r--r-- | lib/gitlab/background_task.rb | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/lib/gitlab/background_task.rb b/lib/gitlab/background_task.rb new file mode 100644 index 00000000000..1f03e32844c --- /dev/null +++ b/lib/gitlab/background_task.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Gitlab + # Used to run small workloads concurrently to other threads in the current process. + # This may be necessary when accessing process state, which cannot be done via + # Sidekiq jobs. + # + # Since the given task is put on its own thread, use instances sparingly and only + # for fast computations since they will compete with other threads such as Puma + # or Sidekiq workers for CPU time and memory. + # + # Good examples: + # - Polling and updating process counters + # - Observing process or thread state + # - Enforcing process limits at the application level + # + # Bad examples: + # - Running database queries + # - Running CPU bound work loads + # + # As a guideline, aim to yield frequently if tasks execute logic in loops by + # making each iteration cheap. If life-cycle callbacks like start and stop + # aren't necessary and the task does not loop, consider just using Thread.new. + # + # rubocop: disable Gitlab/NamespacedClass + class BackgroundTask + AlreadyStartedError = Class.new(StandardError) + + attr_reader :name + + def running? + @state == :running + end + + # Possible options: + # - name [String] used to identify the task in thread listings and logs (defaults to 'background_task') + # - synchronous [Boolean] if true, turns `start` into a blocking call + def initialize(task, **options) + @task = task + @synchronous = options[:synchronous] + @name = options[:name] || self.class.name.demodulize.underscore + # We use a monitor, not a Mutex, because monitors allow for re-entrant locking. + @mutex = ::Monitor.new + @state = :idle + end + + def start + @mutex.synchronize do + raise AlreadyStartedError, "background task #{name} already running on #{@thread}" if running? + + start_task = @task.respond_to?(:start) ? @task.start : true + + if start_task + @state = :running + + at_exit { stop } + + @thread = Thread.new do + Thread.current.name = name + @task.call + end + + @thread.join if @synchronous + end + end + + self + end + + def stop + @mutex.synchronize do + break unless running? + + if @thread + # If thread is not in a stopped state, interrupt it because it may be sleeping. + # This is so we process a stop signal ASAP. + @thread.wakeup if @thread.alive? + begin + # Propagate stop event if supported. + @task.stop if @task.respond_to?(:stop) + + # join will rethrow any error raised on the background thread + @thread.join unless Thread.current == @thread + rescue Exception => ex # rubocop:disable Lint/RescueException + Gitlab::ErrorTracking.track_exception(ex, extra: { reported_by: name }) + end + @thread = nil + end + + @state = :stopped + end + end + end + # rubocop: enable Gitlab/NamespacedClass +end |