Apr 26

Back in Feb I asked on various lists how I could auto-wrap CHI driver methods, but didn’t get any completely satisfying answers:

CHI drivers implement methods like remove() and clear(). If you call $cache->remove(), it goes directly to the driver subclass.

The problem is that there are now legitimate reasons to “wrap” these methods at the CHI/Driver.pm superclass level (meaning, do something before and/or after the method). For example, I want to add an optional generic size-awareness feature (the cache can keep track of its own size), which means that we have to adjust size whenever remove() and clear() are called. And I want to log remove() calls the way we currently log get() and set().

So one solution is to define remove() and clear() in CHI/Driver.pm, and have them call _remove() and _clear() in the driver subclasses. But this kind of change makes me uneasy for several reasons:

  • It changes the driver API, i.e. all existing drivers out there have to modified. And we might have to change it again as we identify new methods to wrap.
  • The list of ‘normal’ versus ‘underscore’ methods becomes rather arbitrary – it’s “whatever we’ve needed to wrap so far”.

I thought about using regular wrapping modules, like Sub::Prepend or Hook::LexWrap. But this fails when you have subclasses more than one level deep. e.g.:

   CHI::Driver -> CHI::Driver::Foo -> CHI::Driver::Foo::Bar

Now if you call CHI::Driver::Foo::Bar::remove(), the wrapping code will get called twice, once for each subclass. I only want it to be called once regardless of how deep the subclass is.

Here’s how I solved this in CHI-0.2. When each CHI driver is used for the first time, e.g. CHI::Driver::Memory:

   my $cache = CHI->new('Memory');

CHI autogenerates a new class called CHI::Wrapped::CHI::Driver::Memory, which inherits from

   ('CHI::Driver::Wrapper', 'CHI::Driver::Memory')

then blesses the actual cache object (and future cache objects of this driver) as CHI::Wrapped::CHI::Driver::Memory.

Now, when someone calls a method like $cache->get() or $cache->remove(), CHI::Driver::Wrapper has an opportunity to handle it first, and then pass control to CHI::Driver::Memory. If not, it goes directly to CHI::Driver::Memory.

I was unable to find this solution on CPAN, even though I feel like I must be reinventing the wheel. If someone knows of a distribution that encapsulates this technique, please let me know.

Here’s the code from CHI::Driver::Wrapper that creates the wrapper class:

   sub create_wrapped_driver_class {
       my ( $proto, $driver_class ) = @_;
       carp "internal class method" if ref($proto);
      
       if ( !$wrapped_driver_classes{$driver_class} ) {
           my $wrapped_driver_class      = "CHI::Wrapped::$driver_class";
           my $wrapped_driver_class_decl = join( "n",
               "package $wrapped_driver_class;",
               "use strict;",
               "use warnings;",
               "use base qw(CHI::Driver::Wrapper $driver_class);",
               "sub driver_class { '$driver_class' }",
               "1;" );
           eval($wrapped_driver_class_decl);    ## no critic ProhibitStringyEval
           die $@ if $@;                        ## no critic RequireCarping
           $wrapped_driver_classes{$driver_class} = $wrapped_driver_class;
       }
       return $wrapped_driver_classes{$driver_class};
   }

And here’s the first application of auto-wrapping: when certain methods are called on a cache, automatically call them on the subcaches, if any.

   # Call these methods first on the main cache, then on any subcaches.
   #
   foreach my $method (qw(remove expire expire_if clear purge)) {
       no strict 'refs';
       *{ __PACKAGE__ . "::$method" } = sub {
           my $self = shift;
           my $retval = $self->call_native_driver( $method, @_ );
           $self->call_method_on_subcaches( $method, @_ );
           return $retval;
       };
   }
   
   # Call the specified $method on the native driver class, e.g. CHI::Driver::Memory.  SUPER
   # cannot be used because it refers to the superclass(es) of the current package and not to
   # the superclass(es) of the object - see perlobj.
   #
   sub call_native_driver {
       my $self                 = shift;
       my $method               = shift;
       my $native_driver_method = join( "::", $self->driver_class, $method );
       $self->$native_driver_method(@_);
   }

(See original use.perl posting)

Apr 25

I’ve just released CHI 0.02. The main visible change is that multi-level caches have been fleshed out and made easier to use.

There are two kinds of multi-level relationships that I wanted to be able to express easily with CHI:

  • L1 (level 1) cache: Sits in front of the primary cache in order faster access for commonly accessed cache entries. i.e. a cache for your cache.
  • Mirror cache: Sits behind the primary cache and, over time, mirrors its contents. Useful for migrating from one cache to another without a sudden performance hit.

Initially CHI had a Multilevel driver that would let you place two or more caches inside a container cache object. The problem was that adding an L1 to an existing cache required changing it to a Multilevel cache, causing existing driver-specific calls to fail. (e.g. If I change a File cache to a Multilevel cache, File-specific methods will no longer get handled right.)

In 0.2 I switched to a primary cache / subcache model, which seems more appropriate. Now the File cache has an L1 subcache, and File-specific methods (as well as many ancillary methods on which the L1 relationship has no clear meaning) simply go to the primary cache.

The usage is also simpler. Here we place an in-process Memory cache in front a Memcached cache:

    my $cache = CHI->new(
        driver   => 'Memcached',
        servers  => [ "10.0.0.15:11211", "10.0.0.15:11212" ],
        l1_cache => { driver => 'Memory' }
    );

Note that there isn’t a way yet to specify a size limit for the memory cache, which would make this a lot more self-maintaining. :) That’s coming soon. In the meantime, I’m planning to use this for an unlimited request-based cache, clearing it manually at the end of each web request:

    $cache->l1_cache->clear();

Here we prepare to migrate from an old to a new cache directory:

    my $cache = CHI->new(
        driver          => 'File',
        root_dir        => '/old/cache/root',
        mirror_cache => { driver => 'File', root_dir => '/new/cache/root' },
    );

We leave this running for a few hours (or as needed), then replace it with

    my $cache = CHI->new(
        driver   => 'File',
        root_dir => '/new/cache/root'
    );

More details in the Subcaches section of the CHI 0.2 documentation.

(See original use.perl posting)

preload preload preload