caching
Caching and streaming
Conditional GET
fresh-when sets validators on the response and renders a 304 Not Modified
when the request's If-None-Match/If-Modified-Since show the client already
has the current version. is-stale is its inverse: it returns true when you
should render.
method show {
self.render('show') if self.is-stale(etag => $post, last-modified => $post.updated-at);
}etag-for computes an ETag from a value, using its cache-key or to-hash when
available. ETags are weak by default (W/"..."); pass weak => False for a
strong ETag. Matching uses a weak comparison, so W/ prefixes are ignored, and
* matches anything.
Cache headers
expires-in sets Cache-Control with a max-age, private by default or
public, plus any extra directives. expires-now marks the response
uncacheable:
self.expires-in(3600, public => True, must-revalidate => True);
# Cache-Control: public, max-age=3600, must-revalidate
self.expires-now; # Cache-Control: no-cache
self.expires-now(no-store => True); # Cache-Control: no-storeCache store
MVC::Keayl::Cache provides a Store role with a low-level key-value API shared
by every backend:
$store.write('user/1', $user, expires-in => 300);
$store.read('user/1');
$store.exist('user/1');
$store.delete('user/1');
$store.fetch('user/1', { load-user(1) }, expires-in => 300);fetch returns a cached value or computes, stores, and returns it. force
recomputes, skip-nil declines to store an undefined result, and
race-condition-ttl serves the stale value to other readers while one recomputes
an expired entry.
increment and decrement maintain counters, preserving an entry's expiry
window across updates. read-multi and write-multi work on several keys at
once, and delete-matched removes every key matching a string glob or a regex:
$store.increment('visits', 1, expires-in => 3600);
$store.write-multi({ a => 1, b => 2 });
$store.read-multi('a', 'b');
$store.delete-matched('user/*');Entries carry an optional version; a read with a mismatched version misses.
A store can take a namespace that prefixes its keys and a default-expires-in
applied when a write gives no expiry.
Backends
MemoryStorekeeps entries in process, with an optionalmax-entriesbound that evicts the least recently used entry.FileStorepersists each entry under arootdirectory, surviving across instances.NullStorestores nothing, sofetchalways recomputes. It is the no-op backend for test and development.ExternalStoredelegates to an injectedclientshaped like a Redis or Memcache driver (get,setwith attl,del,keys), serializing each entry through it.
Fragment caching
A store's fetch backs view fragment caching. cache-key derives a key from
parts (an object contributes its own cache-key), under a views/ namespace,
with an optional template digest:
cache-key($post, digest => template-digest($source)); # views/posts/1-2021/<digest>A view caches a fragment with cache-fragment, which computes the content once
per key through its configured store:
$view.cache-fragment([$post], { render-the-fragment() });Rate limiting
rate-limit is a controller class method that caps how often a client may reach
an action, backed by a cache store. It registers a before-action that counts
requests per discriminator within a window and blocks once the count passes the
limit:
PostsController.rate-limit(to => 100, within => 60);It also has an is rate-limit trait form for the class header:
class PostsController is MVC::Keayl::Controller is rate-limit(to => 100, within => 60) { }By default the discriminator is the request's remote IP and an over-limit request
gets 429 Too Many Requests with a Retry-After header set to the window. by
supplies a custom discriminator and with a custom over-limit handler; store
and name choose the backing store and the counter name:
ApiController.rate-limit(
to => 1000, within => 3600,
by => -> $controller { $controller.api-key },
with => -> $controller { $controller.head(503) },
store => $cache,
);Streaming
A response body can be streamed instead of buffered. stream takes an iterable
of chunks (strings or blobs); stream-chunks yields them encoded, and
is-streaming reports whether a stream is set, for an adapter to write chunked:
$response.stream(gather { for @rows { take row-html($_) } });This is the pull-based primitive: the whole sequence is known up front. For a push-based response, where an action writes chunks over time, see live streaming.
Range requests
send-file honours a Range request header, responding 206 Partial Content
with a Content-Range header and the requested byte slice, and advertises
Accept-Ranges: bytes.