We have recently started using ActiveModel::Serializer at FishBrain in favour of jbuilder. During this ongoing switch we have had to deal with a few issues mainly regarding to caching. If you haven’t used ActiveModel::Serializer before I would recommend you check it out.
There is plenty of documentation for AMS in general, but less so for caching. This post is based on version 0.8.0 which is currently the recommended version.
View Caching
While AMS is quite a bit faster than jbuilder it is still painfuly slow without caching the resulting json in a few scenarios. Luckily caching is extremely simple. In its simplest form this is all it takes
It’s also possible to define a more specialized cache key by implementing cache_key
manually instead.
This caching will work well and give you a huge win in speed. The invalidation is also handled nicely due to the updated_at
property of ActiveRecord’s default cache_key implementation.
Here is an example
post/1-20150121205945000000000
Notice that the updated_at
value is part of the cache key. A simple Post.find(1).touch
will invalidate the cache and prevent staleness of the view.
There is however a big issue here. When the model changes we are fine thanks to updated_at
, but what about when the Serializer changes?
This is how AMS calculates the cache_key in 0.8.0
As seen here the actual serializer is not part of the cache key. This will cause any changes made to serializer to not appear before the cache is invalidated either manually or over time as the updated_at
fields of the models change. ActionView solves this by calculating a Digest for each view. The digest will change when ever the view or any of it’s dependencies change. I will show a simpler approach to solving this problem.
Versioning the view
The general idea is to keep a version number for each serializer, this is quite prone to human error, but with a defined step during code reviews it should be caught. To facilitate this we will create a base serializer with this functionallity.
With this change the new PostSerializer
will look like this
The new cache key will look like this
version/1/post/1-20150121205945000000000
and after AMS applies their extra key components like this:
post_serializer/version/1/post/1-20150121205945000000000
If we now wish to change the serializer by adding a new attribute or changing something we just have to bump the version to 2
when we are done and the cache will invalidate.
Bonus: ETags
I’ve implemented roughly the same thing in our API, but I was also looking into ETagging and HTTP based caching overall. This suffers from the same issue as the view caching.
We can reuse the versioning of the serializers to prevent cache staleness due to change in the serializer too.
Instead of using etag: @post
we will build an etag based on both the model and the view.
We leave the last_modified
set to @post.updated_at
because as long as the Etag changes the request will be considered to be stale by rails.
Warning: If the client only uses If-Modified-Since
and skips If-None-Match
this can still cause staleness of the view. To combat this the version of the Serializer could be changed to a unixtimestamp and the maximum of the version and the model’s updated_at
could be for the last_modified
value.
Removing the human error
The worst part of this approach is that it requires humans to remember the version bump when changing anything. From experience we know that humans aren’t very good at such tasks. I have not come up with a suitable solution for calculating a digest based on the actual code in the Serializer in an efficent way, but this would be preferable. If you think you have a good solution I’d love to hear it.
Nathan Kontny has also published a solution to this problem.