Be lazy, be ESI: HTTP caching and Symfony2 @ PHPDay 2011 05-13-2011
-
Upload
alessandro-nadalin -
Category
Technology
-
view
10.455 -
download
0
description
Transcript of Be lazy, be ESI: HTTP caching and Symfony2 @ PHPDay 2011 05-13-2011
HTTP caching and Symfony2
Be lazy, be ESI
Alessandro NadalinMay, 13 2011
Symfony2 sucksat caching
because it doesn't have an application caching layer
high five, that's great
Sf2 embraces the HTTP caching specification
and that's not because
Fabien islazy
that's because
Fabien kicks asses
HTTP cache
is a
King
in Symfony2
Caching in Symfony2
Caching is, obviously, a matter of the response
since the response is an object,we have some basic OO stuff
to handle HTTP cache
Creating a response
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
return $res;}
Creating a response
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
return $res;}
Creating a response
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
return $res;}
Creating a response
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
return $res;}
Creating a response
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
return $res;}
Creating a response
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
return $res;}
Expiration
Expiration
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
$date = new DateTime(); $date->modify('+600 seconds');
$res->setExpires($date);
return $res;}
Expiration
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
$date = new DateTime(); $date->modify('+600 seconds');
$res->setExpires($date);
return $res;}
Expiration
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
$date = new DateTime(); $date->modify('+600 seconds');
$res->setExpires($date);
return $res;}
Expiration through Cache-Control
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
$res->setMaxAge(600); $res->setSharedMaxAge(600);
return $res;}
Expiration through Cache-Control
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
$res->setMaxAge(600); $res->setSharedMaxAge(600);
return $res;}
Expiration through Cache-Control
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
$res->setMaxAge(600); $res->setSharedMaxAge(600);
return $res;}
Expiration through Cache-Control
public function indexAction(){ $res = $this->render('MyBundle:Default:home.html.twig', array( 'title' => 'My blog', ));
$res->setMaxAge(600); $res->setSharedMaxAge(600);
return $res;}
Shared caches, like Symfony2 reverse proxy
Validation
Validation
public function articleAction($id){ $article = $this->get('entity_manager')->query($id)... $res = $this->render('MyBundle:Default:article.html.twig', array( 'article' => '$article', ));
$etag = "article:{$article->getId()}:{$article->getVersion()}";
$res->setETag($etag); $res->setLastModified($article->getModifiedAt());
return $res;
Validation
public function articleAction($id){ $article = $this->get('entity_manager')->query($id)... $res = $this->render('MyBundle:Default:article.html.twig', array( 'article' => '$article', ));
$etag = "article:{$article->getId()}:{$article->getVersion()}";
$res->setETag($etag); $res->setLastModified($article->getModifiedAt());
return $res;
Validation
public function articleAction($id){ $article = $this->get('entity_manager')->query($id)... $res = $this->render('MyBundle:Default:article.html.twig', array( 'article' => '$article', ));
$etag = "article:{$article->getId()}:{$article->getVersion()}";
$res->setETag($etag); $res->setLastModified($article->getModifiedAt());
return $res;
Validation
public function articleAction($id){ $article = $this->get('entity_manager')->query($id)... $res = $this->render('MyBundle:Default:article.html.twig', array( 'article' => '$article', ));
$etag = "article:{$article->getId()}:{$article->getVersion()}";
$res->setETag($etag); $res->setLastModified($article->getModifiedAt());
return $res;
Validation
public function articleAction($id){ $article = $this->get('entity_manager')->query($id)... $res = $this->render('MyBundle:Default:article.html.twig', array( 'article' => '$article', ));
$etag = "article:{$article->getId()}:{$article->getVersion()}";
$res->setETag($etag); $res->setLastModified($article->getModifiedAt());
return $res;
Datetime object
Automagically get a Datetime from Doctrine2
namespace Acme\BlogBundle\Entity;
/** * @orm:Entity * @orm:Table(name="article") */class Article{ /** * @orm:Column(type="datetime", name="modified_at") */ protected $modifiedAt;
Automagically get a Datetime from Doctrine2
namespace Acme\BlogBundle\Entity;
/** * @orm:Entity * @orm:Table(name="article") */class Article{ /** * @orm:Column(type="datetime", name="modified_at") */ protected $modifiedAt;
but hey, you say
what's the advatage of using validation if we always hit
the DB and create a new Response?
Validation, the right way
public function articleAction($id){ $res = new Response()
$etag = Memcache::get("Article:{$id}:etag"); $lastModified = Memcache::get("Article:{$id}:last_modified");
$res->setETag($etag); $res->setLastModified($lastModified);
if ($res->isNotModified($this->get('request'))) { return $res; }
$article = $this->get('entity_manager')->query($id)... ...
Validation, the right way
public function articleAction($id){ $res = new Response()
$etag = Memcache::get("Article:{$id}:etag"); $lastModified = Memcache::get("Article:{$id}:last_modified");
$res->setETag($etag); $res->setLastModified($lastModified);
if ($res->isNotModified($this->get('request'))) { return $res; }
$article = $this->get('entity_manager')->query($id)... ...
Validation, the right way
public function articleAction($id){ $res = new Response()
$etag = Memcache::get("Article:{$id}:etag"); $lastModified = Memcache::get("Article:{$id}:last_modified");
$res->setETag($etag); $res->setLastModified($lastModified);
if ($res->isNotModified($this->get('request'))) { return $res; }
$article = $this->get('entity_manager')->query($id)... ...
Validation, the right way
public function articleAction($id){ $res = new Response()
$etag = Memcache::get("Article:{$id}:etag"); $lastModified = Memcache::get("Article:{$id}:last_modified");
$res->setETag($etag); $res->setLastModified($lastModified);
if ($res->isNotModified($this->get('request'))) { return $res; }
$article = $this->get('entity_manager')->query($id)... ...
relax
Calculating an Etag, or a date, is cheaperthan generating a full MVC response
Control your power
Additional management
public function articleAction($id){ $res = new Response()
$res->setVary(array( 'Accept-Encoding', ));
$res->setPublic();
$res->setNotModified();
...
Varying the response
public function articleAction($id){ $res = new Response()
$res->setVary(array( 'Accept-Encoding', ));
$res->setPublic();
$res->setNotModified();
...
Cacheable by all caches
public function articleAction($id){ $res = new Response()
$res->setVary(array( 'Accept-Encoding', ));
$res->setPublic();
$res->setNotModified();
...
or by local only
public function articleAction($id){ $res = new Response()
$res->setVary(array( 'Accept-Encoding', ));
$res->setPrivate();
$res->setNotModified();
...
Good old 304
public function articleAction($id){ $res = new Response()
$res->setVary(array( 'Accept-Encoding', ));
$res->setPrivate();
$res->setNotModified();
...
Stale
public function articleAction($id){ $res = new Response()
$res->setVary(array( 'Accept-Encoding', ));
$res->setPrivate();
$res->expire();
...
but hey, you say
HTTP's cache fails when dealing with really dynamic pages, because consumers will always have to hit the origin server, although a part of the page would be cacheable ( header and
footer, for example )
no bueno, you say
NOPE
ESI was built for thathttp://www.w3.org/TR/esi-lang
<esi:include src="http://mysite.com/twitterfeeds.html" />
ESI and Symfony2
# controller$response->setSharedMaxAge(3600);
# app/config/config.ymlframework: esi: { enabled: true }
# template{% render '...:twitterFeeds' with {}, {'standalone': true} %}
# fragment controller$response->setSharedMaxAge(15);
ESI and Symfony2
# controller$response->setSharedMaxAge(3600);
# app/config/config.ymlframework: esi: { enabled: true }
# template{% render '...:twitterFeeds' with {}, {'standalone': true} %}
# fragment controller$response->setSharedMaxAge(15);
ESI and Symfony2
# controller$response->setSharedMaxAge(3600);
# app/config/config.ymlframework: esi: { enabled: true }
# template{% render '...:twitterFeeds' with {}, {'standalone': true} %}
# fragment controller$response->setSharedMaxAge(15);
ESI and Symfony2
# controller$response->setSharedMaxAge(3600);
# app/config/config.ymlframework: esi: { enabled: true }
# template{% render '...:twitterFeeds' with {}, {'standalone': true} %}
# fragment controller$response->setSharedMaxAge(15);
ESI and Symfony2
# controller$response->setSharedMaxAge(3600);
# app/config/config.ymlframework: esi: { enabled: true }
# template{% render '...:twitterFeeds' with {}, {'standalone': true} %}
# fragment controller$response->setSharedMaxAge(15);
Use Symfony2 reverse proxy
Use Symfony2 reverse proxy(slower)
Varnish configuration
backend apache { .host = "127.0.0.1"; .port = "8080";}
sub vcl_recv { unset req.http.Accept-Encoding; unset req.http.Vary; set req.http.Surrogate-Capability = "abc=ESI/1.0";}
sub vcl_fetch { esi;
...
Varnish configuration
backend apache { .host = "127.0.0.1"; .port = "8080";}
sub vcl_recv { unset req.http.Accept-Encoding; unset req.http.Vary; set req.http.Surrogate-Capability = "abc=ESI/1.0";}
sub vcl_fetch { esi;
...}
tell Symfony2 you're behind a reverse proxyable to handle the ESI specification
Why HTTP cachingis so
important?
Ask yourself:as a developer, what do I want
on my application?
Evolve
Loose coupling
Work less
Evolve Because you want your platform to extensible
Loose coupling
Work less
Evolve Because you want your platform to extensible
Loose coupling Because you want it to be easy to integrate with, evolve, plug and mantain
Work less
Evolve Because you want your platform to extensible
Loose coupling Because you want it to be easy to integrate with, evolve, plug and mantain
Work less
Because every LoC is bug-prone and our man-day is a hard-to-scale cost
enters our Hero #1
enters our Hero #2
http://www.lullabot.com/articles/a-beginners-guide-to-caching-data
2007
2011?
it supportsHTTP caching!
http://drupal.org/node/147310
( people is supposed to clap their hands here )
but
wait.... how?
Default headers
Expires = 'Sun, 19 Nov 1978 05:00:00 GMT',
Cache-Control = 'no-cache, must-revalidate',
ETag = $_SERVER['REQUEST_TIME'],
Default headers
Expires = 'Sun, 19 Nov 1978 05:00:00 GMT',
Cache-Control = 'no-cache, must-revalidate',
ETag = $_SERVER['REQUEST_TIME'],
Default headers
Expires = 'Sun, 19 Nov 1978 05:00:00 GMT',
Cache-Control = 'no-cache, must-revalidate',
ETag = $_SERVER['REQUEST_TIME'],
Default headers
Expires = 'Sun, 19 Nov 1978 05:00:00 GMT',
Cache-Control = 'no-cache, must-revalidate',
ETag = $_SERVER['REQUEST_TIME'],
is that even legal?
"but you can redefine them!"
drupal_add_http_header()
function drupal_add_http_header(){ ...
...
drupal_send_headers($headers);}
so, what
drupal_send_headers()
can do so evil?
header(),
of course
which means
drupal_add_http_header('Dumb-Header', 'I\'m batman!');...// other logic...drupal_add_http_header('Dumb-Header', 'I\'m not');
var_dump(headers_list());
drupal_add_http_header('Dumb-Header', 'I\'m batman!');...// other logic...drupal_add_http_header('Dumb-Header', 'I\'m not');
var_dump(headers_list());
array 0 => string 'X-Powered-By: PHP/5.3.2-1ubuntu4.7' 1 => string 'Cache: I'm not'
drupal_add_http_header('Dumb-Header', 'I\-m batman!');...// other logic...drupal_add_http_header('Dumb-Header', false);
var_dump(headers_list());
drupal_add_http_header('Dumb-Header', 'I\-m batman!');...// other logic...drupal_add_http_header('Dumb-Header', false);
var_dump(headers_list());
array 0 => string 'X-Powered-By: PHP/5.3.2-1ubuntu4.7' 1 => string 'Cache: I'm batman'
drupal_add_http_header('Dumb-Header', 'I\-m batman!');...// other logic...drupal_add_http_header('Dumb-Header', ' ');
var_dump(headers_list());
drupal_add_http_header('Dumb-Header', 'I\-m batman!');...// other logic...drupal_add_http_header('Dumb-Header', ' ');
var_dump(headers_list());
array 0 => string 'X-Powered-By: PHP/5.3.2-1ubuntu4.7' 1 => string 'Cache:'
or
you can use header_remove()
( PHP 5.3 )
and create a new class to manage/keep track of
headers and caching directives
but we're lazy
and
we don't want to reinvent the wheel
Goals
Work lessevolve
loose coupling
Goals
Work lessevolve
loose coupling
everything is done for us!
:)
but....
tmp files, cache tables, procedural code...
mmmmh....
gotta be something better
Frameworks
symfony ONE
Cache is used for compiling routes, autoloading, ...
Cache is used for compiling routes, autoloading, ...
...but also for storing the view
Goals
Work lessevolve
loose coupling
Goals
Work lessevolve
loose coupling
at least because we use a framework
HTTP
Goals
Work lessevolve
loose coupling
Less work
because the hard work is delegated to the browser/proxy
Evolve
because cache is abstracted from the application
Loose coupling
because caching is bound to the protocol, HTTP, not to your implementation ( Sf, RoR, Django )
Alessandro Nadalin
Alessandro Nadalin
Alessandro Nadalin
odino.org@_odino_http://joind.in/talk/view/2988
Alessandro Nadalin
odino.org@_odino_http://joind.in/talk/view/2988
Thanks!
Creditshttp://www.flickr.com/photos/snakphotography/5004775320/sizes/o/in/photostream/
http://www.flickr.com/photos/ashatenbroeke/4367373081/sizes/z/in/photostream/http://www.flickr.com/photos/juanpg/3333385784/sizes/z/in/photostream/http://www.flickr.com/photos/congvo/301678287/sizes/l/in/photostream/
http://www.flickr.com/photos/adamrice/280300202/sizes/l/in/photostream/http://www.flickr.com/photos/tomer_a/541411897/sizes/o/in/photostream/http://www.flickr.com/photos/subpra/4514008262/sizes/l/in/photostream/
http://www.flickr.com/photos/lippincott/2539720043/sizes/l/in/photostream/http://www.flickr.com/photos/rawryder/5086090931/sizes/l/in/photostream/http://www.flickr.com/photos/robboudon/5312731161/sizes/l/in/photostream/
http://www.flickr.com/photos/bc-burnslibrary/4158243488/sizes/o/in/photostream/http://www.flickr.com/photos/13606325@N08/2416993706/sizes/o/in/photostream/
http://www.flickr.com/photos/neothezion/5135841069/sizes/l/in/photostream/http://www.flickr.com/photos/planetschwa/2494067809/http://www.flickr.com/photos/thomasthomas/258931782/
http://www.flickr.com/photos/jungle_boy/220181177/sizes/l/in/photostream/http://www.flickr.com/photos/ramduk/3708039504/sizes/z/in/photostream/
http://www.flickr.com/photos/sashawolff/3228711025/sizes/l/in/photostream/