Caching behavior when calling a policy from another via `#allowed_to?`
@palkan thanks for all your work on this, I'm enjoying using it. I've read through the caching section in the docs and based on what's written in the per-thread caching section, I was expecting policies that are instantiated by calls via allowed_to? from other policies to be cached in the Thread store, but for some reason, that is not what I'm seeing. I have a use case somewhat similar to your comments example in the docs where a policy is delegating to another policy via allowed_to?.
To be more specific, I have something like this:
class OrganizationMembershipPolicy < ApplicationPolicy
def update?
if record.owner?
allowed_to?(:manage_owner_memberships?, record.organization)
else
allowed_to?(:manage_memberships?, record.organization)
end
end
end
class OrganizationPolicy < ApplicationPolicy
def manage_memberships?
membership.owner? || membership.admin?
end
def manage_owner_memberships?
membership.owner?
end
# #membership_for makes a call to the database to fetch the membership for the given user
def membership
@membership ||= record.membership_for(user)
end
end
When I fetch many organization memberships for the same organization, iterate through them, and call allowed_to?(:update, organization_membership), the policy for the organization is not cached in the Thread store. Each time I call allowed_to?(:update, organization_membership) a new organization policy is instantiated, which results in an N+1 query due to the membership being fetched in the OrganizationPolicy.
So, if I were to have this in my controller:
organization = Organization.first
organization.organization_memberships.each do |organization_membership|
allowed_to?(:update?, organization_membership)
end
Each iteration is instantiating a new organization policy even though every organization_membership belongs to the same organization. I've added a binding.pry and inspected the Thread store and I see that OrganizationMembershipPolicy instances are stored, but not OrganizationPolicy instances.
Is this intended or am I missing something?
Hey,
Thanks for the detailed report.
Is this intended or am I missing something?
Yes, that's how it's designed.
Per-thread cache works in the context of authorization behavior, i.e., from where you invoke policies. Policies themselves are not behaviors, they're low-level units of authorization work. There is no any cache at the policy level.
However, I don't see why we can't include ActionPolicy::PerThreadCache and benefit from it. I think, it's worth trying.
Per-thread cache works in the context of authorization behavior, i.e., from where you invoke policies. Policies themselves are not behaviors, they're low-level units of authorization work. There is no any cache at the policy level.
Got it, thanks 👍
However, I don't see why we can't include ActionPolicy::PerThreadCache and benefit from it. I think, it's worth trying.
Sounds good. In cases where the policies are used in channels (or similar long-lived contexts), we'd want to avoid including this, correct?
In cases where the policies are used in channels (or similar long-lived contexts), we'd want to avoid including this, correct?
It's fine to use in channels, since they use a Rails executor and we reset the cache on every unit of work (there is no a long-lived thread associated with a connection; there is a pool of threads used to perform actions).