Catalyst-Plugin-PageCache

 view release on metacpan or  search on metacpan

lib/Catalyst/Plugin/PageCache.pm  view on Meta::CPAN

sub _get_page_cache_expiration_time {
    my ($c, $options) = @_;
    my $pc_config = $c->config->{'Plugin::PageCache'};
    my $is_debug = $pc_config->{debug};

    my $expires;

    # Use the explicitely passed in duration if available.
    # XXX but why then ignore it if it's <= 0 ?
    if ($options->{cache_seconds} and $options->{cache_seconds} > 0) {
        $c->log->debug("expires in specified $options->{cache_seconds}s")
            if $is_debug;

        $expires = time() + $options->{cache_seconds};
    }
    # else {
    #   ... calculate expiry based on the response headers
    # }
    # If all else fails, fallback to the default 'expires' configuration value.
    else {
        $c->log->debug("expires in default $pc_config->{expires}s")
            if $is_debug;
        $expires = time() + $pc_config->{expires};
    }

    return $expires;
}


sub dispatch {
    my $c = shift;

    # only serve GET and HEAD request pages from cache
    # (never POST, PUT, DELETE etc)
    return $c->next::method(@_)
        unless $c->req->method =~ m/^(?:GET|HEAD)$/;

    my $pc_config = $c->config->{'Plugin::PageCache'};

    my $hook_name = $pc_config->{cache_dispatch_hook} || $pc_config->{cache_hook};
    my $hook = $hook_name ? $c->can($hook_name) : undef;
    return $c->next::method(@_) if ( $hook && !$c->$hook() );

    return $c->next::method(@_)
      if ( $pc_config->{auto_check_user}
        && $c->can('user_exists')
        && $c->user_exists);

    # check the page cache for a cached copy of this page
    return $c->next::method(@_)
        unless my $key = $c->_get_page_cache_key;

    my $cache = $c->cache; # curry the cache just once, here

    return $c->next::method(@_)
        unless my $data = $cache->get( $key );

    # Time to remove page from cache?

    if ( $data->{expire_time} && $data->{expire_time} <= time ) {
        if ( my $busy_lock = $pc_config->{busy_lock} ) {
            # Extend the expiration time for others while
            # this caller refreshes the cache
            $data->{expire_time} = time() + $busy_lock;
            
            $cache->set( $key, $data );
            
            $c->log->debug( "$key has expired, being refreshed for $busy_lock seconds" )
                if ($pc_config->{debug});
        }
        else {
            $c->log->debug( "Expiring $key from page cache" )
              if ($pc_config->{debug});

            $cache->remove( $key );

            if ( my $index_page_key = $pc_config->{index_page_key} ) {
                my $index = $cache->get( $index_page_key ) || {};
                my $found = delete $index->{ $data->{index_key} };
                $cache->set( $index_page_key, $index, $pc_config->{no_expire})
                    if $found;
            }
        }

        return $c->next::method(@_);
    }

    $c->log->debug("Serving $key from page cache, expires in "
          . ($data->{expire_time} - time)
          . " seconds")
        if ($pc_config->{debug});

    $c->_page_cache_used( 1 );

    # Check If-Modified-Since headers
    return 1 if $c->_page_cache_not_modified( $data );

    # Serve cached page

    $c->res->body( $data->{body} );

    $c->res->content_type( join '; ', @{$data->{content_type}} )
        if $data->{content_type};

    $c->res->content_encoding( $data->{content_encoding} )
        if $data->{content_encoding};

    $c->_set_page_cache_headers( $data );

    $c->res->header('X-PageCache', 'Catalyst');

}

# See if request matches last_modified date in cache
# if so, arrange to return a 304 Not Modified status
# and return true.

sub _page_cache_not_modified {
    my ( $c, $data ) = @_;

    if ( $c->req->headers->if_modified_since ) {

        if ( $c->req->headers->if_modified_since == $data->{create_time} ) {
            $c->res->status(304); # Not Modified
            $c->res->headers->remove_content_headers;
            $c->_set_page_cache_headers( $data );
            $c->res->body( '' );
            return 1;

lib/Catalyst/Plugin/PageCache.pm  view on Meta::CPAN


    $c->log->debug(
        "Caching page $key for ". ($data->{expire_time} - time()) ." seconds"
    ) if ($pc_config->{debug});
    
    if ( $pc_config->{cache_headers} ) {
        $data->{headers} = {
            map { $_ => $headers->header($_) } $headers->header_field_names
        };
    }

    if ($pc_config->{index_page_key}) {
        # We can't simply use $key for $index_key because $key may have been
        # mangled by sha1 and/or a key_maker hook.  We include $key to ensure
        # unique entries in the index in cases where a given uri might produce
        # different results eg due to headers like language.
        $data->{index_key}  = '/'.$c->req->path;
        $data->{index_key} .= '?'.$c->req->uri->query if $c->req->uri->query;
        $data->{index_key} .= '#'.$key;
    }

    if (exists $options->{expires}) {
        $data->{expires} = $options->{expires}
    }

    my $cache = $c->cache; # curry the cache just once, here

    $cache->set( $key, $data );

    $c->_set_page_cache_headers( $data );  # don't forget the first time

    if ( $data->{index_key} ) {
        # Keep an index cache of all pages that have been cached, for use
        # with clear_cached_page. This is error prone. See KNOWN ISSUES.
        my $index_page_key = $pc_config->{index_page_key};
        my $index = $cache->get($index_page_key) || {};
        $index->{ $data->{index_key} } = $key;
        $cache->set($index_page_key, $index, $pc_config->{no_expire});
    }

    return $data;
}


sub setup {
    my $c = shift;

    $c->next::method(@_);

    # allow code using old config key to work
    if ( $c->config->{page_cache} and !$c->config->{'Plugin::PageCache'} ) {
        $c->config->{'Plugin::PageCache'} = delete $c->config->{page_cache};
    }

    my $pc_config = $c->config->{'Plugin::PageCache'} ||= {};

    $pc_config->{auto_cache}       ||= [];
    $pc_config->{expires}          ||= 60 * 5;
    $pc_config->{cache_headers}    ||= 0;
    $pc_config->{set_http_headers} ||= 0;
    $pc_config->{busy_lock}        ||= 0;
    $pc_config->{debug}            ||= $c->debug;

    # default the page key to include the app name to give some measure
    # of protection if the cache doesn't have a namespace set.
    $pc_config->{index_page_key} = "$c._page_cache_index"
        unless defined $pc_config->{index_page_key};

    if (not defined $pc_config->{disable_index}) {
        warn "Plugin::PageCache config does not include disable_index, which currently defaults false but may default true in future\n";
        $pc_config->{disable_index} = 0;
    }
    $pc_config->{index_page_key} = undef
        if $pc_config->{disable_index};

    # detect the cache plugin being used and set appropriate
    # never-expires syntax
    if ( $c->can('cache') ) {

        # Newer C::P::Cache, cannot call $c->cache as a package method
        if ( $c->isa('Catalyst::Plugin::Cache') ) {
            return;
        }

        my $cache = $c->cache; # curry the cache just once, here

        # Older Cache plugins
        if ( $cache->isa('Cache::FileCache') ) {
            $pc_config->{no_expire} = "never";
        }
        elsif ($cache->isa('Cache::Memcached')
            || $cache->isa('Cache::FastMmap'))
        {

          # Memcached defaults to 'never' when not given an expiration
          # In FastMmap, it's not possible to set an expiration
            $pc_config->{no_expire} = undef;
        }
    }
    else {
        die __PACKAGE__ . " requires a Catalyst::Plugin::Cache plugin.";
    }
}

sub _get_page_cache_key {
    my $c = shift;
    
    # We can't rely on the params after the user's code has run,
    # so we cache the key created during the initial dispatch phase
    # and reuse it at finalize time.
    return $c->_page_cache_key if ( $c->_page_cache_key );

    # override key if required, else use uri path
    my $keymaker = $c->config->{'Plugin::PageCache'}->{key_maker};
    my $key = $keymaker ? $keymaker->($c) : "/" . $c->req->path;
    
    # prepend language if I18N present.
    if ( $c->can('language') ) {
        $key = ':' . $c->language . ':' . $key;
    }

lib/Catalyst/Plugin/PageCache.pm  view on Meta::CPAN

another controller.  For example, if you cache a page behind a login screen,
the logged-in version may be cached and served to unauthenticated users.

Note that pages that result from POST requests will never be cached.

=head1 PERFORMANCE

On my Athlon XP 1800+ Linux server, a cached page is served in 0.008 seconds
when using the HTTP::Daemon server and any of the Cache plugins.

=head1 CONFIGURATION

Configuration is optional.  You may define the following configuration values:

    expires => $seconds

This will set the default expiration time for all page caches.  If you do not
specify this, expiration defaults to 300 seconds (5 minutes).

    cache_headers => 1

Enable this value if you need your cached responses to include custom HTTP
headers set by your application.  This may be necessary if you operate behind
an edge cache such as Akamai.  This option is disabled by default.

    set_http_headers => 1

Enabling this value will cause Catalyst to set the correct HTTP headers to
allow browsers and proxy servers to cache your page.  This will further reduce
the load on your server.  The headers are set in such a way that the
browser/proxy cache will expire at the same time as your cache.  The
Last-Modified header will be preserved if you have already specified it.  This
option is disabled by default.

    auto_cache => [
        $uri,
    ]

To automatically cache certain pages, or all pages, you can specify auto-cache
URIs as an array reference.  Any controller within your application that
matches one of the auto_cache URIs will be cached using the default expiration
time.  URIs may be specified as absolute: '/list' or as a regex: '/view/.*'

    disable_index => 1

To support the L</clear_cached_page> method, PageCache attempts keep an index
of all cached pages. This adds overhead by performing extra cache reads and
writes to maintain the (possibly very large) page index. It's also not
reliable, see L</KNOWN ISSUES>.

If you don't intend to use C<clear_cached_page>, you should enable this config
option to avoid the overhead of creating and updating the cache index.  This
option is currently disabled (i.e. the page index is enabled) by default but
that may change in a future release.

    index_page_key => '...'

The key string used for the index, Defaults to a string that includes the name
of the Catalyst app class.

    busy_lock => 10

On a high traffic site where page re-generation may take many seconds, a common
problem encountered is the "dog-pile" effect, where many concurrent connections all
hit a page where the cache has expired and all perform the same expensive operation
to rebuild the cache.  To prevent this situation, you can set the busy_lock option
to the maximum number of seconds any of your pages can be expected to take to
rebuild the cache.  Then, when the cache expires, the first request will rebuild the
cache while also extending the expiration time by the number of seconds specified,
allowing other requests that arrive before the cache has been rebuilt to use the
previously cached page.  This option is disabled by default.

    debug => 1

This will print additional debugging information to the Catalyst log.  You will
need to have -Debug enabled to see these messages.

    auto_check_user => 1

If this option is enabled, automatic caching is disabled for logged in users
i.e., if the app class has a user_exists() method and it returns true.

    cache_hook => 'cache_hook_method'
    cache_finalize_hook => 'cache_finalize_hook_method'
    cache_dispatch_hook => 'cache_dispatch_hook_method'

Calls a method on the application that is expected to return a true or false.
This method is called before dispatch, and before finalize so you can short
circuit the pagecache behavior.  As an example, if you want to disable
PageCache while running under debug mode:
   
    package MyApp;
    
    ...
    
    sub cache_hook_method { return shift->debug; }

Or, if you want to not cache for certain roles, say "admin":
    
    sub cache_hook_method {
        my ( $c ) = @_;
        return !$c->check_user_roles('admin');
    }

Note that this is called BEFORE auto_check_user, so you have more flexibility
to determine what to do for not logged in users.

To override the generation of page keys:

    __PACKAGE__->config(
        'Plugin::PageCache' => {
            key_maker => sub {
                my $c = shift;
                return $c->req->base . '/' . $c->req->path;
            }
        }
    );

C<key_maker> can also be the name of a method, which will be invoked as C<<$c->$key_maker>>.

In most cases you would use a single cache_hook method for consistency.

It is possible to achieve background refreshing of content by disabling
caching in cache_dispatch_hook and enabling caching in cache_finalize_hook
for a specific IP address (say 127.0.0.1).



( run in 1.041 second using v1.01-cache-2.11-cpan-39bf76dae61 )