Manage concurrency and advisory locks
masterMethods like #rebuild and #find_or_create_by_path are not safe for concurrent execution and can cause data corruption or duplicate nodes. Closure Tree uses with_advisory_lock to ensure correctness across PostgreSQL and MySQL.
Disabling locks:
You can disable advisory locks by passing with_advisory_lock: false. Warning: If you disable this and perform multi-threaded writes without an alternative mutex, you will eventually experience data corruption.
Customizing lock names: You can customize the advisory lock name to avoid collisions or to implement multi-tenancy. Supported types:
- Static String:
advisory_lock_name: 'custom_lock' - Proc (1-arity): Receives the model class.
advisory_lock_name: ->(model_class) { ... } - Model Method: Delegates to a class method.
advisory_lock_name: :custom_lock_name - Proc (2-arity): Receives the model class and the instance. This is recommended for scoped/multi-tenant models to ensure each tenant has its own lock.
# Static string
class Tag < ApplicationRecord
has_closure_tree advisory_lock_name: 'custom_tag_lock'
end
# Dynamic via Proc (1-arity)
class Tag < ApplicationRecord
has_closure_tree advisory_lock_name: ->(model_class) { "#{Rails.env}_#{model_class.name.underscore}" }
end
# Delegate to model method
class Tag < ApplicationRecord
has_closure_tree advisory_lock_name: :custom_lock_name
def self.custom_lock_name
"tag_lock_#{current_tenant_id}"
end
end
# Per-instance lock names (2-arity) for multi-tenancy
class Node < ApplicationRecord
has_closure_tree scope: :company_id,
advisory_lock_name: ->(klass, instance) {
company = instance&.company_id
company ? "ct_#{klass.name}_#{company}" : "ct_#{klass.name}"
}
end