module ForemanTasks class Lock < ActiveRecord::Base LINK_LOCK_NAME = :link_resource OWNER_LOCK_NAME = :task_owner # not really intended to be created in database, but it's used for # explicitly stating that the all the locks for resource should be used ALL_LOCK_NAME = :all RESERVED_LOCK_NAMES = [LINK_LOCK_NAME, OWNER_LOCK_NAME, ALL_LOCK_NAME].freeze class LockConflict < StandardError attr_reader :required_lock, :conflicting_locks def initialize(required_lock, conflicting_locks) header = _('Required lock is already taken by other running tasks.') header << "\n" header << _('Please inspect their state, fix their errors and resume them.') header << "\n\n" header << _('Required lock: %s') % required_lock.name header << "\n" header << _('Conflicts with tasks:') header << "\n" url_helpers = Rails.application.routes.url_helpers conflicting_tasks = conflicting_locks .map(&:task) .uniq .map { |task| "- #{Setting['foreman_url'] + url_helpers.foreman_tasks_task_path(task)}" } .join("\n") super header + conflicting_tasks @required_lock = required_lock @conflicting_locks = conflicting_locks end end belongs_to :task belongs_to :resource, polymorphic: true scope :active, -> { joins(:task).where('foreman_tasks_tasks.state != ?', :stopped) } validates :task_id, :name, :resource_id, :resource_type, presence: true validate do raise LockConflict.new(self, colliding_locks) unless available? end # returns true if it's possible to aquire this kind of lock def available? !colliding_locks.exists? end # returns a scope of the locks colliding with this one def colliding_locks task_ids = task.self_and_parents.map(&:id) colliding_locks_scope = Lock.active.where(Lock.arel_table[:task_id].not_in(task_ids)) colliding_locks_scope = colliding_locks_scope.where(name: name, resource_id: resource_id, resource_type: resource_type) unless exclusive? colliding_locks_scope = colliding_locks_scope.where(:exclusive => true) end colliding_locks_scope end class << self # Locks the resource so that no other task can lock it while running. # No other task related to the resource is not allowed (even not-locking ones) # A typical usecase is resource deletion, where it's good idea to make sure # nothing else related to the resource is really running. def exclusive!(resource, uuid) build_exclusive_locks(resource, uuid).each(&:save!) end def exclusive?(resource) build_exclusive_locks(resource).all?(&:available?) end # Locks the resource so that no other task can lock it while running. # Other not-locking tasks are tolerated. # # The lock names allow to specify what locks should be activated. It has to # be a subset of names defined in model's class available_locks method # # When no lock name is specified, the resource is locked against all the available # locks. # # It also looks at +related_resources+ method of the resource to calcuate all # the related resources (recursively) and links the task to them as well. def lock!(resource, uuid, *lock_names) build_locks(resource, lock_names, uuid).each(&:save!) end def lockable?(resource, uuid, *lock_names) build_locks(resource, lock_names, uuid).all?(&:available?) end def locked?(resource, uuid, *lock_names) !lockable?(resource, uuid, *lock_names) end def colliding_locks(resource, uuid, *lock_names) build_locks(resource, lock_names, uuid) .inject([]) { |collisions, lock| collisions.concat lock.colliding_locks.to_a } end # Assigns the resource to the task to easily track the task in context of # the resource. This doesn't prevent other actions to lock the resource # and should be used only for actions that tolerate other actions to be # performed on the resource. Usually, this shouldn't needed to be done # through the action directly, because the lock should assign it's parrent # objects to the action srecursively (using +related_resources+ method in model # objects) def link!(resource, uuid) build_link(resource, uuid).save! end def link?(resource, uuid) build_link(resource, uuid).available? end # Records the information about the user that triggered the task def owner!(user, uuid) build_owner(user, uuid).save! end private def all_lock_names(resource, include_links = false) lock_names = [] if resource.class.respond_to?(:available_locks) && resource.class.available_locks.any? lock_names.concat(resource.class.available_locks) else raise "The resource #{resource.class.name} doesn't define any available lock" end if lock_names.any? { |lock_name| RESERVED_LOCK_NAMES.include?(lock_name) } raise "Lock name #{lock_name} is reserved" end lock_names.concat([LINK_LOCK_NAME, OWNER_LOCK_NAME]) if include_links lock_names end def build_exclusive_locks(resource, uuid = nil) build_locks(resource, all_lock_names(resource, true), uuid) end def build_locks(resource, lock_names, uuid = nil) locks = [] if lock_names.empty? || lock_names == [:all] lock_names = all_lock_names(resource) end lock_names.map do |lock_name| locks << build(uuid, resource, lock_name, true) end locks.concat(build_links(resource, uuid)) locks end def build_links(resource, uuid = nil) related_resources(resource).map do |related_resource| build_link(related_resource, uuid) end end def build_link(resource, uuid = nil) build(uuid, resource, LINK_LOCK_NAME, false) end def build_owner(user, uuid = nil) build(uuid, user, OWNER_LOCK_NAME, false) end def build(uuid, resource, lock_name, exclusive) new(task_id: uuid, name: lock_name, resource_type: resource.class.name, resource_id: resource.id, exclusive: !!exclusive) end # recursively search for related resources of the resource (using # the +related_resources+ method, avoiding the cycles def related_resources(resource) if resource.respond_to?(:all_related_resources) resource.all_related_resources else [] end end end end end