96 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			96 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			Ruby
		
	
	
	
| # 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
 |