Cache::Memcached::Managed provides namespace support. You're correct that it won't be as fast as the best local caches. If you weren't already planning to use memcached, you might try just making a cache table in MySQL instead. It could be a simple HEAP table, with a key for the namespace in addition to they primary key. | [reply] |
Thanks for that Perrin - I wasn't aware of the ::Managed version of memcached, and it looks like it could solve some other issues in my system, but probably not for privileges.
Privileges may not change that frequently, but when they do change, they can have a far reaching effect, so it makes more sense to take the hit of working out which cached values to expire at the moment that something changes, rather than working out the namespaces right from the beginning.
However, I think your suggestion of using a memory table in MySQL for the privilege cache is probably spot on. It'd be fast, centralised and easy to manage, and comes with the indexes I need to expire the changed values intelligently.
The only problem I can see is the possibility of two processes interacting, where one caches a value low down in the privilege tree, while the other is changing a value higher up - I'll probably need to work through that with locking... which should be fine because these are all fast simple queries.
thanks - heart more at rest now...
| [reply] |
You're getting ahead of yourself. You don't say whether or not you even have a basic version working that takes a second per check. Get something functional and put tests around it so that every optimization can still be checked for correctness. It does no good to have it return in 10 milliseconds if the answer is wrong 10% of the time.
My criteria for good software:
- Does it work?
- Can someone else come in, make a change, and be reasonably certain no bugs were introduced?
| [reply] |
I do have a working version but is has the problem of speed for the initial request, and then the changing of a high level intersection of privileges which would affect many cached values. At the moment, if privileges change, I'm just emptying the entire privilege cache, which is not very efficient.
So the system works, I just think that it could be better, faster and more scalable, and my question is whether my proposed solution sounds is good : to maintain speed and accuracy at the the expense of table space.
The code for checking the inherited permissions is as follows:
#===================================
sub inherited_permission {
#===================================
my $self = shift;
unless (defined $self->{_inh}) {
my $object = $self->object;
my $object_parent_id = $object->parent_id;
my @object_groups = $object->groups;
my $own_object_id = $object->id;
my @object_ids = (
$own_object_id,
@object_groups,
$object_parent_id
);
my $subject = $self->subject;
my $subject_parent_id = $subject->parent_id;
my @subject_groups = $subject->groups;
my $own_subject_id = $subject->id;
my @subject_ids = (
$own_subject_id,
@subject_groups,
$subject_parent_id
);
my $inherited_permission = $self->permission;
foreach my $object_id (@object_ids) {
foreach my $subject_id (@subject_ids) {
next if !($subject_id && $object_id)
|| ($subject_id == $own_subject_id
&& $object_id == $own_object_id);
my $permission = $self->new({
object => $self->base_class->new($object_id),
subject => $self->base_class->new($subject_id)
});
$inherited_permission|=$permission->inherited_permissi
+on;
}
}
$self->{_inh} = $inherited_permission & $self->mask;
my @saved = delete @{$self}{'_subject','_object'};
$self->save_to_cache;
@{$self}{'_subject','_object'}= @saved;
}
return $self->{_inh};
}
(There is some added complexity involved because in my live system, the actual privileges reported depend on the 'status' of each object, so an album of status 'awaiting approval' would grant different privileges to an album of status 'approved'). This is just handled by a series of predefined masks. | [reply] [d/l] |
Clinton,
Since you asked me via email to comment on this query, with specific reference to Bricolage, let me tell you how it works in Bricolage.
First, there are no permissions granted to individual users or to individual objects. It just made the schema too complex. So we only have groups of users and groups of objects that function as subjects and objects in Bricolage.
When you load an object from the database, the IDs of the groups of which it is a member are also loaded, in the same query. This list of group IDs is available via a call to get_grp_ids().
To check a permission, an object is passed to the user object's can_do() method, along with the permission in question. So if I want to know if a user has Edit permission to an object, I simply do something like this: if ($user->can_do($obj, EDIT)) {...}. The can_do() method then compares the object's group IDs against an ACL loaded for the user.
The user object is cached in the session, so it only gets loaded once for each user. Whenever permissions change, a flag is set in the system-wide cache and all user sessions automatically reload the user whenever it is set, so that permission changes are always immediate. This is not ideal, but generally expiring all users is more efficient that expiring all objects.
The ACL contains a hash mapping object group IDs to their permissions. So all can_do() has to do is iterate over this hash, find all of the relevant group IDs that the object is in, and compare the permissions.
Now, I wrote this a _long_ time ago, and it's far from ideal. It used to be that each object had to have its group IDs loaded in a separate query, and as you can imagine this made permission checking (and therefore Bricolage) extremely slow. It was _much_ better after all objects started loading their group IDs at the same time that they were loaded.
Now, as to your questions, I have the following feedback:
- Make your objects always load their own ACLs at the same time that they're loaded, so that you don't have to send a separate query for every object for which you want to check the permissions.
- If you do store calculated permissions in the database, use triggers to keep them updated, rather than a cron job. Then you won't have to think about them and they'll always be up-to-date. Besides, the data isn't really redundant because it's a calculated sum that must be dynamically maintained for every object. It makes sense to cache it like this.
- Do use memcached or a MySQL table for centralized caching. The performance difference is not worth the bother compared to the convenience of centralized caching for multiple servers. It works great for LiveJournal, it can work for you. Perrin nails this one.
HTH,
—Theory
| [reply] [d/l] [select] |