Skip Menu |

This queue is for tickets about the Catalyst-Plugin-PageCache CPAN distribution.

Report information
The Basics
Id: 53303
Status: open
Priority: 0/
Queue: Catalyst-Plugin-PageCache

People
Owner: Nobody in particular
Requestors: rod.taylor [...] gmail.com
Cc:
AdminCc:

Bug Information
Severity: Wishlist
Broken in: (no value)
Fixed in: (no value)



Subject: Allow PageCache to use HTTP Headers for expiry time
Consolidates the logic of selecting an expiration time to a single function and adds in the ability (used by default) for HTTP headers to provide the values.

It consider Cache-Control and Expiry. This is a work in progress and no tests have been added yet. The real catch is the module now requires DateTime::Format::HTTP in order to parse the Expiry header.


The big upshot of this is that Applications setting a Cache-Control header for client browsers will now automatically take benefit of that same value for PageCache based caching.


Current state of progress attached and may be applied. More testing for the headers should be done.

Subject: PageCache.pm.patch
*** Catalyst-Plugin-PageCache-0.22.orig/lib/Catalyst/Plugin/PageCache.pm 2009-06-25 06:16:57.000000000 -0400 --- Catalyst-Plugin-PageCache-0.22/lib/Catalyst/Plugin/PageCache.pm 2010-01-02 19:45:08.000000000 -0500 *************** *** 3,8 **** --- 3,10 ---- use strict; use base qw/Class::Accessor::Fast/; use MRO::Compat; + use DateTime; + use DateTime::Format::HTTP; our $VERSION = '0.22'; *************** *** 37,48 **** $expires = $expires->epoch - time if ref($expires) && $expires->isa('DateTime'); ! $expires ||= $c->config->{'Plugin::PageCache'}->{expires}; ! # mark the page for caching during finalize ! if ( $expires > 0 ) { ! $c->_cache_page( { cache_seconds => $expires } ); ! } } sub clear_cached_page { --- 39,51 ---- $expires = $expires->epoch - time if ref($expires) && $expires->isa('DateTime'); ! # Mark that cache_page has been called but defer on selecting the time period ! # to cache for until finalize. _get_expiration_time has more options at that ! # time, such as various headers which may have been set. ! my $options = {}; ! $options->{cache_seconds} = $expires if ($expires and $expires > 0); ! $c->_cache_page( $options ); } sub clear_cached_page { *************** *** 81,86 **** --- 84,154 ---- } } + + # Internal function to return the time that the item should expire + # from the cache using all available resources, including HTTP headers + # of the outgoing response. + sub _get_expiration_time { + my ($c, $options) = @_; + my $is_debug = $c->config->{'Plugin::PageCache'}->{debug}; + + my $expires; + + # Use the explicitely passed in time if available. + if ($options->{cache_seconds} and $options->{cache_seconds} > 0) { + $c->log->debug("Set expires time via provided value") if ($is_debug); + + $expires = time() + $options->{cache_seconds}; + } + + # Use the value from Cache-Control if available. Respect no-cache and no-store + # directives as well by setting $expires to immediate. + elsif (my $cc_header = $c->response->headers->header('Cache-Control')) { + $c->log->debug("Set expires time via Cache-Control header: $cc_header") if ($is_debug); + if ($cc_header !~ 'no-cache|no-store|must-revalidate') { # Expire immediately + $expires = time(); + } + + # s-maxage overrides max-age and the Expires header + # Both s-maxage and max-age coould be provided so we use two separate + # if blocks to handle them in the correct order. + elsif ($cc_header =~ m/^s-maxage\s*=\s*(\d+)/g) { + my $seconds = $1; + $expires = time() + $seconds; + } + elsif ($cc_header =~ m/%^max-age\s*=\s*(\d+)/g) { + my $seconds = $1; + $expires = time() + $seconds; + } + + # Sent by itself, we are allowed to cache for some unspecified time period. + # Use the default amount provided by the configuration file. + # Private is okay since we assume the catalyst server will be private. + elsif ($cc_header =~ 'public|private') { + $expires = time() + $c->config->{'Plugin::PageCache'}->{expires}; + } + + } + # Look for and use the value from the Expires header if configured + elsif (my $expiry_header = $c->response->headers->header('Expires')) { + $c->log->debug("Set expires time via Expires header: $expiry_header") if ($is_debug); + + my $dt = DateTime::Format::HTTP->parse_datetime($expiry_header); + + if ($dt and $dt->epoch() > time()) { + $expires = $dt->epoch(); + } + } + + # If all else fails, fallback to the default configuration value. + else { + $c->log->debug("Set expires time via to default time") if ($is_debug); + $expires = time() + $c->config->{'Plugin::PageCache'}->{expires} + } + + return $expires; + } + sub dispatch { my $c = shift; *************** *** 272,281 **** my $now = time; - $c->log->debug( - "Caching page $key for $options->{cache_seconds} seconds" - ) if ($c->config->{'Plugin::PageCache'}->{debug}); - # Cache some additional metadata along with the content # Some caches don't support expirations, so we do it manually my $data = { --- 340,345 ---- *************** *** 285,292 **** create_time => $options->{last_modified} || $c->res->headers->last_modified || $now, ! expire_time => $now + $options->{cache_seconds}, }; if ( $c->config->{'Plugin::PageCache'}->{cache_headers} ) { $data->{headers} = { --- 349,361 ---- create_time => $options->{last_modified} || $c->res->headers->last_modified || $now, ! expire_time => $c->_get_expiration_time($options), }; + + $c->log->debug( + "Caching page $key for ". ($data->{expire_time} - time()) ." seconds" + ) if ($c->config->{'Plugin::PageCache'}->{debug}); + if ( $c->config->{'Plugin::PageCache'}->{cache_headers} ) { $data->{headers} = { *************** *** 466,471 **** --- 535,543 ---- This plugin requires that you also load a Cache plugin. Please see the Known Issues when choosing a cache backend. + When possible PageCache will use the existing Content-Control or Expires headers + for defining the duration a page will remain in the cache. + =head1 WARNINGS PageCache should be placed at the end of your plugin list.
From: rod.taylor [...] gmail.com
Fixed a few dumb mistakes and I output an info() line when the Cache-Control header is not recognized.<br /> <br /> Works pretty well now but it should still have a test or two. <br /> <br />
Subject: headers.patch
*** Catalyst-Plugin-PageCache_ORIGINAL/lib/Catalyst/Plugin/PageCache.pm 2009-06-25 06:16:57.000000000 -0400 --- Catalyst-Plugin-PageCache_HEADERS/lib/Catalyst/Plugin/PageCache.pm 2010-01-02 21:26:21.000000000 -0500 *************** *** 3,8 **** --- 3,10 ---- use strict; use base qw/Class::Accessor::Fast/; use MRO::Compat; + use DateTime; + use DateTime::Format::HTTP; our $VERSION = '0.22'; *************** *** 37,48 **** $expires = $expires->epoch - time if ref($expires) && $expires->isa('DateTime'); ! $expires ||= $c->config->{'Plugin::PageCache'}->{expires}; ! # mark the page for caching during finalize ! if ( $expires > 0 ) { ! $c->_cache_page( { cache_seconds => $expires } ); ! } } sub clear_cached_page { --- 39,51 ---- $expires = $expires->epoch - time if ref($expires) && $expires->isa('DateTime'); ! # Mark that cache_page has been called but defer on selecting the time period ! # to cache for until finalize. _get_expiration_time has more options at that ! # time, such as various headers which may have been set. ! my $options = {}; ! $options->{cache_seconds} = $expires if ($expires and $expires > 0); ! $c->_cache_page( $options ); } sub clear_cached_page { *************** *** 81,86 **** --- 84,157 ---- } } + + # Internal function to return the time that the item should expire + # from the cache using all available resources, including HTTP headers + # of the outgoing response. + sub _get_expiration_time { + my ($c, $options) = @_; + my $is_debug = $c->config->{'Plugin::PageCache'}->{debug}; + + my $expires; + + # Use the explicitely passed in time if available. + if ($options->{cache_seconds} and $options->{cache_seconds} > 0) { + $c->log->debug("Set expires time via provided value") if ($is_debug); + + $expires = time() + $options->{cache_seconds}; + } + + # Use the value from Cache-Control if available. Respect no-cache and no-store + # directives as well by setting $expires to immediate. + elsif (my $cc_header = $c->response->headers->header('Cache-Control')) { + $c->log->debug("Found Cache-Control: $cc_header") if ($is_debug); + if ($cc_header =~ /no-cache|no-store|must-revalidate/) { # Expire immediately + $expires = time(); + } + + # s-maxage overrides max-age and the Expires header + # Both s-maxage and max-age coould be provided so we use two separate + # if blocks to handle them in the correct order. + elsif ($cc_header =~ m/^s-maxage\s*=\s*(\d+)/g) { + my $seconds = $1; + $expires = time() + $seconds; + } + elsif ($cc_header =~ m/%^max-age\s*=\s*(\d+)/g) { + my $seconds = $1; + $expires = time() + $seconds; + } + + # Sent by itself, we are allowed to cache for some unspecified time period. + # Use the default amount provided by the configuration file. + # Private is okay since we assume the catalyst server will be private. + elsif ($cc_header =~ 'public|private') { + $expires = time() + $c->config->{'Plugin::PageCache'}->{expires}; + } + else { + $c->log->info("Expiring Immediately due to unrecognized Cache-Control value: $cc_header"); + $expires = time(); + } + } + # Look for and use the value from the Expires header if configured + elsif (my $expiry_header = $c->response->headers->header('Expires')) { + $c->log->debug("Set expires time via Expires header: $expiry_header") if ($is_debug); + + my $dt = DateTime::Format::HTTP->parse_datetime($expiry_header); + + if ($dt and $dt->epoch() > time()) { + $expires = $dt->epoch(); + } + } + + # If all else fails, fallback to the default configuration value. + else { + $c->log->debug("Set expires time via to default time") if ($is_debug); + $expires = time() + $c->config->{'Plugin::PageCache'}->{expires} + } + + return $expires; + } + sub dispatch { my $c = shift; *************** *** 272,281 **** my $now = time; - $c->log->debug( - "Caching page $key for $options->{cache_seconds} seconds" - ) if ($c->config->{'Plugin::PageCache'}->{debug}); - # Cache some additional metadata along with the content # Some caches don't support expirations, so we do it manually my $data = { --- 343,348 ---- *************** *** 285,292 **** create_time => $options->{last_modified} || $c->res->headers->last_modified || $now, ! expire_time => $now + $options->{cache_seconds}, }; if ( $c->config->{'Plugin::PageCache'}->{cache_headers} ) { $data->{headers} = { --- 352,364 ---- create_time => $options->{last_modified} || $c->res->headers->last_modified || $now, ! expire_time => $c->_get_expiration_time($options), }; + + $c->log->debug( + "Caching page $key for ". ($data->{expire_time} - time()) ." seconds" + ) if ($c->config->{'Plugin::PageCache'}->{debug}); + if ( $c->config->{'Plugin::PageCache'}->{cache_headers} ) { $data->{headers} = { *************** *** 466,471 **** --- 538,546 ---- This plugin requires that you also load a Cache plugin. Please see the Known Issues when choosing a cache backend. + When possible PageCache will use the existing Content-Control or Expires headers + for defining the duration a page will remain in the cache. + =head1 WARNINGS PageCache should be placed at the end of your plugin list.
I haven't applied the patch. Instead I've performed some refactoring that will simplify a future patch if you choose to send one. I'll leave the ticket open for now.