A couple of weeks ago I decided to internally improve my website. Here I explain exactly what is it that I did to do so. Some of these changes include ruby memoize/memoization and Sinatra Caching.
As I previously mentioned, my blog’s architechture is inspired in [@cyx]’s personal
website. However, I was not totally comfortable with the fact that everytime
that a page was requested (a blog post in this case), the application had to
go to disk, bring the blgo post, parse the markdown and then render the view.
It was something more or less like this:
# app.rb
class App < Sinatra::Application
get '/articles/:article' do
if @article = Article.find(params[:article])
erb :article
else
raise Sinatra::NotFound
end
end
end
# models/article.rb
class Article < OpenStruct
# - Instance Methods - #
def content
RDiscount.new(File.read File.join root, 'articles', file).to_html if file
end
# - Class Methods - #
class << self
def all
YAML.load_file(db_path).map { |article| new article }
end
def find(slug)
all.select { |article| article.file.include? slug }.first
end
protected
def db_path
File.join root, 'db', 'articles.yml'
end
end
end
And in the view I had something very simple, more or less like this:
<%= @article.content %>
Everything was pretty simple. In the end, it’s a blog, right? There’s no reason
to be fancy and make very complicated things. But, well, at the same time, this
is my personal website, and it deserves as much love as any other application
that I code, paid or unpaid. So, I thought that the first thing I could do was
to cache the array of articles and also the returned value by Article#content
.
It was also a really good opportunity to abstract the logic that I had repeated
in between the models Article and Reading, so I ended up with something like this:
(code has been reduced):
# lib/model.rb
class Model < OpenStruct
# - Class Methods - #
class << self
def all
@all ||= YAML.load_file(db) { |record| new record }
end
end
end
# models/article.rb
class Artcle < Model
# - Instance Methods - #
def initialize(args)
super args
self.content
self
end
def content
@content ||= RDiscount.new(File.read path).to_html
end
end
Now, every time that the Article#all
method is called, all the articles are
going to be memoized and each time that a new instance of an article is created
the markdown is going to be parsed and memoized.
But none of this would happen until the first person visited the list of articles.
Every time that a new version of the application is deployed to production, though,
I could call the list in the config.ru file to make a pre-initialization caching.
Let’s see:
# config.ru
require './models/article'
require './models/reading'
require './app'
# Preload articles and readings.
Article.all
Reading.all
run Application
That’s it. Now everytime that someone visits the list of articles and/or readings, the arrays are already loaded in memory with articles and readings ready to be served, even with the markdown parsed!
The next step was to migrate my site to AngularJS. For this I had to serve
my resources (articles and readings) via JSON. Easy. I’m already using OpenStruct
,
so the last step was to serve the hash of my instances formatted to JSON:
# lib/model.rb
class Model < OpenStruct
# - Instance Methods - #
def to_json(options = {})
@json ||= to_h.to_json options
end
end
Because the content doesn’t change that much, I can cache my JSON response, so:
# lib/cache_helpers.rb
module CacheHelpers
def cache_array!(array)
if should_cache?
cache_control :public, :must_revalidate
etag md5 array.to_s
end
end
protected
def md5(string)
Digest::MD5.hexdigest string
end
def should_cache?
settings.production?
end
end
# app.rb
class Application < Sinatra::Application
include CacheHelpers
get '/api/v1/articles' do
@articles = get_articles
cache_articles! @articles
json @articles
end
end
The CacheHelpers#md5
method generates a hash from a string (the JSON array of my
articles and whatever), then I send that value through the ETag header.
And that’s it. Whenever I add a new article, I commit the change, push it to GitHub
and then I deploy the changes into produciton using Mina and then I order Puma
to restart and that’s it.