BigBinary Blog

We write about Ruby on Rails, React.js, React Native, remote work, open source, engineering and design.

Reduce asset delivery time from 30 to 3 seconds with CDN

AceInvoice, one of BigBinary's products, was facing high page load times. AceInvoice is a React.js application, and the size of application.js has gone up to 689kB compressed. Folks from India would sometimes have to wait up to 30 whole seconds for the application.js to load.

AceInvoice is hosted at heroku. Heroku serves assets from a limited number of servers in select locations. The farther you are from these servers, the higher the latency of asset delivery. This is where a CDN like Cloudfront can help.

How does Cloudfront work?

Simply stated CDNs are like caches. CDNs caches recently viewed items and then these CDNs can access stuff from the cache at great speeds.

Let’s take a practical example
  • Create a Cloudfront distribution aceinvoice.cloudfront.com which points to app.aceinvoice.com.
  • The browser makes a request to abc.cloudfront.com/images/peter.jpg for the first time.
  • Cloudfront checks if it has anything cached against /images/peter.jpg. Since it’s the first time it’s encountering this URL it won’t find anything, so it’s a cache miss.
  • Cloudfront forwards this request back to the origin, which in this case is app.aceinvoice.com/images/peter.jpg. The browser gets back the image.
  • During this process, Cloudfront caches the resource. Think of it like a key-value pair where /images/peter.jg is the key and the actual image is the value.
Now let's consider another scenario
  • Another browser makes a request to the same resource.
  • Cloudfront checks for cached items for that particular path.
  • Cloudfront finds the cached resource. It’s a cache hit!
  • Cloudfront directly serves the resource back to the browser without hitting the origin server.

So how is this faster?

Cloudfront has 100+ edge locations scattered around the world. There will always be an edge that’s close to you. Cached resources are immediately made available from all these edge locations. This reduces latency.

What are the caveats?

The biggest issue with using a CDN is properly invalidating caches. Let’s continue with the above example. If the peter.jpg file is updated, Cloudfront is unaware of this change. It’ll keep serving the old file whenever a request is made to that path.

The easiest way to invalidate the cache is by using a hash in the asset name that changes on deploy. Rails handles this by default. After deploying the application, the path to the aforementioned asset might be images/Peter-5nbd44gfae.jpg. When a request comes to this path, Cloudfront caches it and uses the cache for subsequent requests.

But on the next deploy, the path to the same asset changes. Since Cloudfront doesn’t have anything cached for that URL, it will check the origin and get the latest asset.


How to set up Cloudfront with Rails

Rails makes it easy to set up an asset host. In the config/environments/production.rb file, add the following line.

config.action_controller.asset_host = ENV[CLOUDFRONT_ENDPOINT]

By doing this, Rails will look for all the assets in Cloudfront.

Let's consider the application.js asset. In the main .erb file the src for application.js was /packs/js/application.js.

Once we make this change it will be https://ENV[CLOUDFRONT_ENDPOINT]/packs/js/application.js.

We will be setting the environment variable in Heroku shortly.

Creating a Cloudfront distribution
  1. Go to AWS Management Console -> Cloudfront -> Create Distribution Choose Web as the delivery method. Setting delivery method

  2. In origin domain name, specify the path to your server. In this example it is app.aceinvoice.com. In origin protocol policy, choose Match viewer so that the same protocol as the main request is used when Cloudfront forwards requests to the origin server. You can leave the other settings unchanged. Origin settings

  3. In the cache behaviour settings, change viewer protocol policy to Redirect HTTP to HTTPS. You can leave the other settings untouched. Cache behavior

  4. At the bottom of the page switch the distribution state to Enabled and click on Create Distribution. Distribution state

  5. Note the Domain name of your distribution from the Cloudfront dashboard. Cloudfront dashboard

  6. In the Heroku dashboard add the environment variable in your app. Setting environment variable

Result

Serving time of application.js on a cold load (not cached in browser) dropped from 30 seconds to 2-3 seconds.

Vinay Chandran in Rails
Nov 24, 2020
Share

Rails 6.1 adds values_at attribute method for Active Record

This blog is part of our Rails 6.1 series.

Rails 6.1 simplifies retrieving values of attributes on the Active Record model instance by adding the values_at attribute method. This is similar to the values_at method in Hash and Array.

Let's check out an example of extracting values from a User model instance.


class User < ApplicationRecord
  def full_name
    "#{self.first_name} #{self.last_name}"
  end
end

 >> user = User.new(first_name: 'Era', last_name: 'Das' , email: 'era@gmail.com')

=> User id: nil, first_name: "Era", last_name: "Das", created_at: nil, updated_at: nil, email: "era@gmail.com", password_digest: nil

Before Rails 6.1

As shown below using values_at for full_name, which is a method, returns nil.

>> user.attributes.values_at("first_name", "full_name")
=> ["Era", nil]

After changes in Rails 6.1

Rails 6.1 added the values_at method on Active Record which returns an array containing the values associated with the given methods.

>> user.values_at("first_name", "full_name")
=> ["Era", "Era Das"]

Check out the pull request for more details.

Chetan Gawai in Rails 6.1
Nov 17, 2020
Share

Ruby 3 adds new method Hash#except

This blog is part of our Ruby 3.0 series.

Ruby 3 adds a new method, except, to the Hash class. Hash#except returns a hash excluding the given keys and their values.

Why do we need Hash#except?

At times, we need to print or log everything except some sensitive data. Let's say we want to print user details in the logs but not passwords.

Before Ruby 3 we could have achieved it in the following ways:


irb(main):001:0> user_details = { name: 'Akhil', age: 25, address: 'India', password: 'T:%g6R' }

# 1. Reject with a block and include?
irb(main):003:0> puts user_details.reject { |key, _| key == :password }
=> { name: 'Akhil', age: 25, address: 'India' }

# 2. Clone the hash with dup, tap into it and delete that key/value from the clone
irb(main):005:0> puts user_details.dup.tap { |hash| hash.delete(:password) }
=> { name: 'Akhil', age: 25, address: 'India' }

We know that ActiveSupport already comes with Hash#except but for a simple Ruby application using ActiveSupport would be overkill.

Ruby 3

To make the above task easier and more explicit, Ruby 3 adds Hash#except to return a hash excluding the given keys and their values:


irb(main):001:0> user_details = { name: 'Akhil', age: 25, address: 'India', password: 'T:%g6R' }
irb(main):002:0> puts user_details.except(:password)
=> { name: 'Akhil', age: 25, address: 'India' }

irb(main):003:0> db_info = YAML.safe_load(File.read('./database.yml'))
irb(main):004:0> puts db_info.except(:username, :password)
=> { port: 5432, database_name: 'example_db_production' }

Check out the commit for more details. Discussion around it can be found here.

Akhil Gautam in Ruby, Ruby 3
Nov 11, 2020
Share

Database tasks can skip test database using SKIP_TEST_DATABASE

This blog is part of our Rails 6.1 series.

In Rails 6.1, Rails will skip modifications to the test database if SKIP_TEST_DATABASE is set to true.

Without the environment variable


> bundle exec rake db:create
Created database 'app_name_development'
Created database 'app_name_test'

With the environment variable


> SKIP_TEST_DATABASE=true bundle exec rake db:create
Created database 'app_name_development'

As we can see in the first example, both a development and a test database were created, which is unexpected when directly invoking db:create. One obvious solution to this problem is to force the development environment to only create a development database. However this solution will break bin/setup as mentioned in this commit. Hence the need for an environment variable to skip test database creation.

Check out the pull request for more details.

Sandip Mane in Rails 6.1
Oct 27, 2020
Share

Rails 6.1 supports ORDER BY clause for batch processing methods

This blog is part of our Rails 6.1 series.

Before Rails 6.1, batch processing methods like find_each, find_in_batches and in_batches didn't support the ORDER BY clause. By default the order was set to id ASC.

> User.find_each{|user| puts user.inspect}

User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1000]]

Rails 6.1 now supports ORDER BY id for ActiveRecord batch processing methods like find_each, find_in_batches, and in_batches. This would allow us to retrieve the records in ascending or descending order of ID.

> User.find_each(order: :desc){|user| puts user.inspect}

User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1000]]
> User.find_in_batches(order: :desc) do |users|
>   users.each do |user|
>     puts user.inspect
>   end
> end

User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1000]]
> User.in_batches(order: :desc) do |users|
>   users.each do |user|
>     puts user.inspect
>   end
> end

(0.2ms)  SELECT "users"."id" FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1000]]
User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ?  [["id", 101]]

Points to remember:

  • The ORDER BY clause only works with the primary key column.
  • Valid values for the ORDER BY clause are [:asc,:desc] and it's case sensitive. If we use caps or title case (like DESC or Asc) then we'll get an ArgumentError as shown below.
> User.find_in_batches(order: :DESC) do |users|
>   users.each do |user|
>     puts user.inspect
>   end
> end

Traceback (most recent call last):
        2: from (irb):5
        1: from (irb):6:in `rescue in irb_binding'
ArgumentError (unknown keyword: :order)

Check out the pull request for more details.

Sagar Patil in Rails 6.1
Oct 22, 2020
Share

Subscribe to our newsletter