0
votes

I'm working on a multi-tenant platform where our customers can create online stores. A store might might be selling products in multiple currencies, like EUR and USD at the same time.

Since the prices of the products displayed differ based on the currency, I would like to have pages cached for both eur and usd.

The initial part of getting it to work was rather easy, but I encountered an undesired side effect.

sub vcl_recv {
    if (req.http.cookie) {
        cookie.parse(req.http.cookie);
        // If the user has a `currency` cookie, set it as a header
        set req.http.x-currency = cookie.get("currency");
    }
    ...
}

sub vcl_hash {
    hash_data(req.http.x-currency);
}

My backend recognizes uses this header, or its absence, in order to render the proper output and then Varnish stores the result. If the request does not have this header, the server sends a Set-Cookie header, which will tell the browser to create a cookie with the default currency of the store.

My problem stems from the fact that, in advance, I don't have a way of knowing the default currency of a store ( EUR or USD in our case ), because the owner can add/remove them dynamically.

So, on the first request to the /homepage endpoint, the user will not have a currency cookie, so its value will be '', and this will be used in the hash_data method.

The server recognizes that the client did not have a currency cookie, so it set a Set-Cookie header, and when the page hits the browser, the client will have the cookie.

Currently, we have a cache entry for /homepage with no currency cookie.

The customer hits refresh and the request reaches Varnish.

This time, there is a currency cookie, which is used in the hash_data function, but it produces a different key, so it hits the backend again, but the output will be exactly the same ( obviously, Varnish does not know that ).

The /homepage url has two associated cache entries with it, which both have the same content.

Is there an alternative approach I can take to work around this, or a fix of some sort?

My initial train of thought was:

Go inside of the vcl_backend_response subroutine and inspect the request. If the request is missing a cookie, then the beresp object will contain the default currency of the store in its Set-Header. And I can use that to create 2 different cache keys, which point to the same cache entry - /homepage + '' for empty cookie and /homepage + 'EUR'.

That's not technically possible, but its an illustration of something which seems to be fixing my problem.

I have not provided a 100% of my vcl configuration for simplicity, but I have used the default vcls as templates to create a customized configuration for vcl_recv and vcl_backend_response. I know that cookies mean Personalized content and I shouldn't cache that, but the personalization in this case does not work uniquely for this user, so I made an exception.

Edit

www.mystore.com/en/ - default currency is USD

sub vcl_hash {
    if(req.url !~ "^/(contact|sitemap)") {
        hash_data(req.http.x-currency);
    }
}

We have nothing in the cache at the moment

  1. User visits the homepage for the first time, he has no cookies whatsoever
  2. Varnish generates a hash based on http.x-currency, which is empty. The result is a miss.
  3. Varnish hits the backend, the backend sees the lack of the x-currency header, so it sets a Set-Cookie header, which sets the cookie to USD
  4. Varnish receives the response, caches it, and sends it back to the client
  5. The same user hits a refresh on the same page
  6. Varnish generates a hash based on http.x-currency, which is now USD ( it was empty last time ), which results in a cache miss, again
  7. Varnish goes to the backend, the backend returns the exact same response, because no x-currency or x-currency = USD means the same for the backend
  8. Varnish receives the response, caches it and returns it to the client

The homepage route is now completely cached. No matter if somebody visits with currency=USD cookie, or without one, he receives a cached response. However, there are two entries in the cache register, one for an empty currency cookie, and one with currency=USD cookie, and both of these have the same result.

The problem stems from the fact that, when a request arrives and it doesn't have a currency cookie, I don't have a predictable way of figuring out its value without the help of the backend.

It appears to me as though I would just have to live with this.

1

1 Answers

1
votes

As I understand, you only want to create cache variations for pages that contain different currencies. Based on your information, I'm assuming that the homepage doesn't differ in terms of content and doesn't need the variation.

URL matching

You can match certain URL patterns that don't need the variations and conditionally perform the variation.

Here's an example:

sub vcl_hash {
    if(req.url !~ "^/(homepage|contact|sitemap)") {
        hash_data(req.http.x-currency);
    }
}

This is an oversimplified example, but I hope you understand that variations can be done conditionally. Either using inclusion or exclusion patterns.

Sending a Vary header

You can also manage the conditional variation aspect in your application by sending the following Vary response header for pages that need to be varied per currency:

Vary: x-currency

By doing this, you can manage variations from within your application and not depend on hash_data() in sub vcl_hash.

Keep in mind that the x-currency request header needs to be send to the application. That's not really a problem because you have set this header in vcl_recv.

It is important to strip off this part of the Vary header in vcl_deliver because the client has no awareness of this request header.

The duplicate key for the homepage problem

In your (updated) question above, as well as in the comments below, you talk about the hypothetical /homepage object being cached multiple times based on the x-currency header.

This shouldn't really be a problem for you. I noticed that you understand how hashing in Varnish works.

The 2 solutions I mentioned above (URL matching & Vary) explain how to conditionally extend the hash. The term conditionally is very important here.

If the /homepage route produces exactly the same about regardless of the x-currency value, it means that x-currency shouldn't be part of the hash. This also means that the URL and host header will form the basis of the hash.

By not adding x-currency in hash_data() or as part of the Vary header, there will be only one version for /homepage.

You indicated that Vary: x-currency is your preferred solution. I would advise you to only set it for pages that have a different output per currency.

I'm under the impression your problem will be solved by conditionally varying on the currency.

Another important remark is about pages that return a Set-Cookie header if the currency is not set.

Varnish's standard behavior will result in a so-called Hit-For-Miss.

This means that the object is not stored in cache, because you don't want to cache the Set-Cookie header for everyone.

For the next 2 minutes, all requests to that resource will bypass the cache, unless the next response becomes cacheable.

In your case the 2nd request to /homepage will become cacheable, because the response will no longer contain a Set-Cookie.

The result in terms of hit rate is that the first request for every user will be a cache miss because of the Set-Cookie.

Please keep this in mind.

Personally I wouldn't be worried about this, but if you do, you can actually strip off beresp.http.Set-Cookie to make the page cacheable and set the cookie yourself in vcl_deliver.

Of course you'll need to have access to the logic that decides on EUR versus USD to correctly set the cookie value.