# frozen_string_literal: true module GoodJob class BatchRecord < BaseRecord include AdvisoryLockable self.table_name = 'good_job_batches' has_many :jobs, class_name: 'GoodJob::Job', inverse_of: :batch, foreign_key: :batch_id, dependent: nil has_many :executions, class_name: 'GoodJob::Execution', foreign_key: :batch_id, inverse_of: :batch, dependent: nil has_many :callback_jobs, class_name: 'GoodJob::Job', foreign_key: :batch_callback_id, dependent: nil # rubocop:disable Rails/InverseOf scope :finished, -> { where.not(finished_at: nil) } scope :discarded, -> { where.not(discarded_at: nil) } scope :not_discarded, -> { where(discarded_at: nil) } scope :succeeded, -> { finished.not_discarded } alias_attribute :enqueued?, :enqueued_at alias_attribute :discarded?, :discarded_at alias_attribute :finished?, :finished_at scope :display_all, (lambda do |after_created_at: nil, after_id: nil| query = order(created_at: :desc, id: :desc) if after_created_at.present? && after_id.present? query = query.where(Arel.sql('(created_at, id) < (:after_created_at, :after_id)'), after_created_at: after_created_at, after_id: after_id) elsif after_created_at.present? query = query.where(Arel.sql('(after_created_at) < (:after_created_at)'), after_created_at: after_created_at) end query end) # Whether the batch has finished and no jobs were discarded # @return [Boolean] def succeeded? !discarded? && finished? end def to_batch Batch.new(_record: self) end def display_attributes attributes.except('serialized_properties').merge(properties: properties) end def _continue_discard_or_finish(execution = nil, lock: true) execution_discarded = execution && execution.error.present? && execution.retried_good_job_id.nil? take_advisory_lock(lock) do Batch.within_thread(batch_id: nil, batch_callback_id: id) do reload if execution_discarded && !discarded_at update(discarded_at: Time.current) on_discard.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :discard }) if on_discard.present? end if enqueued_at && !finished_at && jobs.where(finished_at: nil).count.zero? update(finished_at: Time.current) on_success.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :success }) if !discarded_at && on_success.present? on_finish.constantize.set(priority: callback_priority, queue: callback_queue_name).perform_later(to_batch, { event: :finish }) if on_finish.present? end end end end class PropertySerializer def self.dump(value) ActiveJob::Arguments.serialize([value]).first end def self.load(value) ActiveJob::Arguments.deserialize([value]).first end end if Rails.gem_version < Gem::Version.new('6.1.0.alpha') # serialize does not yet take a default value, must set via Attributes API attribute :serialized_properties, :json, default: -> { {} } serialize :serialized_properties, PropertySerializer elsif Rails.gem_version < Gem::Version.new('7.1.0.alpha') serialize :serialized_properties, PropertySerializer, default: -> { {} } else serialize :serialized_properties, coder: PropertySerializer, default: -> { {} } end alias_attribute :properties, :serialized_properties def properties=(value) raise ArgumentError, "Properties must be a Hash" unless value.is_a?(Hash) self.serialized_properties = value end private def take_advisory_lock(value, &block) if value with_advisory_lock(function: "pg_advisory_lock", &block) else yield end end end end