CHI::D...opment(3pm)

πŸ“¦
version 0.61 Β· 4 versions
, in debian bookworm, debian trixie, ubuntu jammy, ubuntu noble, ubuntu resolute, ubuntu stonking

✨ CHI::Driver::Development ✨

okay bestie, gather round πŸ’– this is the manual for developing new CHI drivers, aka the User Contributed Perl Documentation page for CHI::Driver::Development. it's giving "you're about to build your own cache backend and I'm SO proud of you already" 🫢


🌸 VERSION 🌸

version 0.61 β€” yes it's a real version number, no cap.


πŸŽ€ SYNOPSIS πŸŽ€

here's the vibe, the whole skeleton of a driver, laid out for you:

package CHI::Driver::MyDriver;
use Moo;
use strict;
use warnings;

extends 'CHI::Driver';

has ...;

sub fetch {
    my ( $self, $key ) = @_;

}

sub store {
    my ( $self, $key, $data[, $expires_in] ) = @_;

}

sub remove {
    my ( $self, $key ) = @_;

}

sub clear {
    my ($self) = @_;

}

sub get_keys {
    my ($self) = @_;

}

sub get_namespaces {
    my ($self) = @_;

}

πŸ¦‹ DESCRIPTION πŸ¦‹

This document describes how to implement a new CHI driver. periodt.

The easiest way to start is to look at existing drivers, such as CHI::Driver::Memory and CHI::Driver::FastMmap. literally just read somebody else's homework, that's the move πŸ™ˆ


πŸ’œ NAMING πŸ’œ

If you are going to publicly release your driver, call it 'CHI::Driver::something' so that users can create it with

CHI->new(driver => 'I<something>');

If it's an internal driver, you can call it whatever you like and create it like

CHI->new(driver => '+My::Internal::CHI::Driver');

see that lil +? that's the "this one's mine, don't @ me" prefix πŸ’…


🌈 MOO / MOOSE 🌈

CHI driver classes must be Moo or Moose based to be fully functional, since we use Moose roles to implement various features. For backward compatibility, non-Moo/Moose drivers will still work at a basic level, but you will see an error if using any feature requiring a role. so like… technically you can rawdog it, but you'll get yelled at the second you want the fancy stuff 😭

All drivers must directly or indirectly extend CHI::Driver. non-negotiable, that's the bloodline, that's the lineage πŸ‘‘


⭐ NAMESPACE ⭐

All cache handles have an assigned namespace that you can access with $self->namespace. You should use the namespace to partition your data store. That is, two cache objects with different namespaces should be able to access the same key without any collision. nobody wants two besties fighting over the same key, it's embarrassing πŸ’€

Examples:

  • The Memory driver uses a separate sub-hash inside its main memory hash for each namespace.
  • The File driver uses a separate top-level directory for each namespace.
  • The FastMmap driver uses a separate Cache::FastMmap file for each namespace.

πŸ”₯ METHODS πŸ”₯

πŸ‘‘ Required methods

The following methods have no default implementation, and MUST be defined by your subclass. no shortcuts, no defaults, you're on your own for these ones:

  • store ( $self, $key, $data[, $expires_in] ) β€” Associate $data with $key in the namespace, overwriting any existing entry. Called by "set". $data will contain any necessary metadata, including expiration options, so you can just store it as a single block. basically: don't overthink it, just yeet the whole blob in there 🫢

$expires_in is optionally the number of seconds from now when the entry will expire. This will only be passed if "expires_on_backend" in CHI is set. If your driver does not support expiration, or if you'd rather just let CHI manage expiration, you can ignore this. ignoring things is self care, fr fr.

  • fetch ( $self, $key ) β€” Returns the data associated with $key in the namespace. Called by "get". The main CHI::Driver superclass will take care of extracting out metadata like expiration options and determining if the value has expired. so you just hand over the goods and let the parent do the bookkeeping ✨
  • remove ( $self, $key ) β€” Remove the data associated with the $key in the namespace. gone, deleted, blocked 🚫
  • clear ( $self ) β€” Remove all data associated with the namespace. (Technically not required, but the default implementation, which iterates over all keys and calls "remove" for each, is very inefficient). like, it works, but it's giving "one item at a time at the checkout with a full cart" energy. please override it πŸ™

🧚 Overridable methods

The following methods have a default implementation, but MAY be overridden by your subclass. they're already cute, but you can make them cuter:

  • BUILD ( $self, $options ) β€” Define the BUILD method if you want to process any options specific to your driver. This is a standard Moo/Moose feature. standard!! classic!! iconic!!
  • fetch_multi_hashref ( $keys ) β€” Override this if you want to efficiently process multiple fetches. Return a hash reference from keys to fetched data. If a key is not available, it may be left out of the hash or paired with undef. The default method will iterate over $keys and call fetch for each. (which, again, is the slow way β€” bulk it up bestie πŸ’…)

This method is called by get_multi_arrayref and get_multi_hashref.

  • store_multi ( $key_data, $options ) β€” Override this if you want to efficiently process multiple stores. $key_data is a hash of keys and data that should be stored. The default will iterate over $key_data and call store for each pair. one by one by one… we're tired 😭

This method is called by set_multi.

πŸ’« Optional methods

The following methods have no default implementation, and MAY be defined by your subclass, but are not required for basic cache operations. they're the little extras, the accessories to the outfit πŸŽ€:

  • get_keys ( $self ) β€” Return all keys in the namespace. It is acceptable to either include or omit expired keys. your call, no judgement here.
  • get_namespaces ( $self ) β€” Return namespaces associated with the cache. It is acceptable to either include or omit namespaces with no valid keys. again, either is fine, live your truth.

πŸ“ DISCARD POLICIES πŸ“

You can create new discard policies for size aware caches, to choose items to discard when the cache gets full. For example, the Memory driver implements an LRU policy. it's the "who's least relevant rn, they gotta go" system πŸ’€

To implement a discard policy foo, define a subroutine discard_policy_foo, which takes a driver object and returns a closure that returns one key each time it is called. The closure should maintain state so that each key is only returned once. one and done, no reruns, we don't do repeats.

For example, here's the Memory driver's LRU implementation. It utilizes a hash containing the last used time for each key.

sub discard_policy_lru {
    my ($self) = @_;

    my $last_used_time = $self->{metadata_for_namespace}->{last_used_time};
    my @keys_in_lru_order =
      sort { $last_used_time->{$a} <=> $last_used_time->{$b} } $self->get_keys;
    return sub {
        shift(@keys_in_lru_order);
    };
}

You can set the default discard policy for your driver by overriding default_discard_policy; otherwise the default is 'arbitrary'. yes, "arbitrary" is a real policy name and honestly? relatable.

sub default_discard_policy { 'lru' }

πŸ§ͺ TESTING πŸ§ͺ

CHI has a standard set of unit tests that should be used to ensure your driver is fully implementing the CHI API. do NOT skip the tests, the tests are the vibe check πŸ”

To use CHI's tests (replacing MyDriver with the name of your driver):

  • Install Test::Class and add it to the build dependencies for your distribution.
  • Add a module called CHI::Driver::MyDriver::t::CHIDriverTests to your distribution containing:
package CHI::Driver::MyDriver::t::CHIDriverTests;
use strict;
use warnings;
use CHI::Test;
use base qw(CHI::t::Driver);

sub testing_driver_class { 'CHI::Driver::MyDriver' }

sub new_cache_options {
    my $self = shift;

    return (
        $self->SUPER::new_cache_options(),

        # Any necessary CHI->new parameters for your test driver
    );
}

1;
  • Add a test script called t/CHI-driver-tests.t to your distribution containing:
#!perl -w
use strict;
use warnings;
use CHI::Driver::MyDriver::t::CHIDriverTests;
CHI::Driver::MyDriver::t::CHIDriverTests->runtests;
  • You may need to override other methods in CHI::Driver::MyDriver::t::CHIDriverTests, e.g. to skip tests that do not apply to your driver. See CHI::t::Driver::Memory and CHI::t::Driver::File in this distribution for examples. peek at the examples, they're right there, they're literally free 🫢

🧹 Test cleanup

You are responsible for cleaning up your datastore after tests are done. The easiest way to do this is to place your datastore wholly inside a temporary directory, or use a guard to remove it at process end. leave no trace, like a ghost but polite πŸ‘»

For example, the File, FastMmap, and DBI tests place all data inside a tempdir that is automatically cleaned up at process end. that's the standard, that's the blueprint.


πŸ‘€ SEE ALSO πŸ‘€

CHI β€” go read it, it's the parent, it's the blueprint, it's the moment.


πŸ’– AUTHOR πŸ’–

Jonathan Swartz <swartz@pobox.com> β€” the legend, the icon, the moment.


This software is copyright (c) 2021 by Jonathan Swartz.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. free as in freedom, slay πŸ’…


okay that's the whole page bestie!! now go build a driver, I believe in you πŸ’–βœ¨πŸŽ€

and for the record, this little gem came from perl v5.32.1, dated 2021-10-15, filed under CHI::D...opment(3pm) β€” the truncated name is not a typo, it's just the man page being dramatic about long titles. mwah πŸ’‹

πŸ“¦ where this page comes from