Going crazy with Varnish

Caching pages of logged in users


Symfony Con, December 6th, 2018


© David Buchmann

HTTP Caching

What is a reverse proxy again?

What about the real live?

Caching content that is not the same for all users

Avoid Sessions

Avoid Session

{% if app.request.hasPreviousSession %}
    {% for message in app.flashes('notice') %}
        <div class="flash-notice">
            {{ message }}
        </div>
    {% endfor %}
{% endif %}
            

Cleanup Cookies: Remove all but session cookie

sub vcl_recv {
    # using a capturing sub pattern, extract the continuous string of
    # alphanumerics that immediately follows "PHPSESSID="
    set req.http.X-Varnish-PHP_SID = regsuball(req.http.Cookie,
                        ";? ?PHPSESSID=([a-zA-Z0-9]+)( |;| ;).*","\1");
    if (req.X-Varnish-PHP_SID != "") {
        set req.http.Cookie = req.X-Varnish-PHP_SID;
    } else {
        unset req.http.Cookie;
    }
    unset req.X-Varnish-PHP_SID;
}
            

https://www.varnish-cache.org/docs/4.1/users-guide/increasing-your-hitrate.html




Logic in the Frontend

Render varying parts in Javascript

$(document).ready(function () {
    if (is_editor()) {
        $("#labels-edit").show();
        $("#milestone-edit").show();
        $("#assignee-edit").show();
    }
});
            

Logic in the Frontend




Ajax for User Specific Parts

Ajax

$(document).ready(function () {
    $.ajax({
        url: "/sidebar.html"
    }).done(function( html ) {
        $( "#sidebar" ).append( html );
    });
});
            

Ajax

Caching despite cookies

Caching despite cookies

Make Symfony keep the cache headers

// Symfony 3.4+ overwrites caching headers when session was used.
// Need to explicitly tell it we intend to cache the response

use Symfony\Component\HttpKernel\EventListener\AbstractSessionListener;
...
$response->headers->set(
    AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true');

builtin.vcl

sub vcl_recv {
    // ...
    if (req.method != "GET" && req.method != "HEAD") {
        /* We only deal with GET and HEAD by default */
        return (pass);
    }
    if (req.http.Authorization || req.http.Cookie) {
        /* Not cacheable by default */
        return (pass);
    }

    return (hash);
}
            

Cache lookup despite cookies

// default.vcl
sub vcl_recv {
    // ...
    if (req.method != "GET" && req.method != "HEAD") {
        /* We only deal with GET and HEAD by default */
        return (pass);
    }

    // Cache lookup even with Cookie or Authorization header
    return (hash);
}
            



Edge Side Includes

Use Edge Side Includes

Like server side include, but on Varnish:

ESI HTML

<html>
    <body>
        Main body.
        <esi:include src="fragment.php" />
    </body>
</html>
            

And activate ESI in Varnish

ESI

Symfony has built-in ESI support

# app/config/config.yml
framework:
  esi: { enabled: true }
  fragments: { path: /_fragment }
            
Make sure /_fragment is not reachable from the outside
{# index.html.twig #}
{% render_esi(controller( 'AppBundle:Comments:comments', {'param': 42 })) %}
            




Cache still either global or individual

User Context

Introducing the User Context Hash

User Context


sub vcl_recv { ...
  // Copy client request headers to auth request
  curl.header_add_all();
  // We go through varnish itself to cache
  curl.header_add("Host: auth");
  curl.get("http://localhost/");

  if (200 != curl.status()) {
      return (synth(curl.status()));
  }
  set req.http.X-User-Context =
        curl.header("X-User-Context");
  curl.free();
... }
sub vcl_recv {
    // Cache auth lookup
    if ("auth" == req.http.Host)
    {
        if (!client.ip ~ self) {
            return (synth(405, "Not allowed"));
        }

        set req.backend_hint = auth;
        // force caching despite auth headers
        return (hash);
    }
    ...
}
            
class RoleProvider implements ContextProvider
{
  public function updateUserContext(
    UserContext $context
  ) {
    ...
    $roles = array_map(function (Role $role) {
        return $role->getRole();
    }, $token->getRoles());

    // Order should not change hash
    sort($roles);
    $context->addParameter('roles', $roles);
  }
}



Wrap Up

Mix solutions

Write your own context hash provider

FOSHttpCacheBundle



Questions / Input / Feedback ?


https://joind.in/talk/9fb1a


Twitter: @dbu