Ruby 2.5 enables Thread.report_on_exception by default and we get more info when threads die

This blog is part of our Ruby 2.5 series. Ruby 2.5 was recently released.

Let’s see what happens when an exception is raised inside a thread.

division_thread = Thread.new do
  puts "Calculating 4/0 in division_thread"
  puts "Result is: #{4/0}"
  puts "Exiting from division_thread"
end

sleep 1

puts "In the main thread"

Execution of it looks like this.

$ RBENV_VERSION=2.4.0 ruby thread_example_1.rb

Calculating 4/0 in division_thread

In the main thread

Note that the last two lines from the block were not printed. Also notice that after failing in the thread the program continued to run in main thread. That’s why we got the message “In the main thread”.

This is because the default behavior of Ruby is to silently ignore exceptions in threads and then to continue to execute in the main thread.

Enabling abort_on_exception to stop on failure

If we want an exception in a thread to stop further processing both in the thread and in the main thread then we can enable Thread[.#]abort_on_exception on that thread to achieve that.

Notice that in the below code we are using Thread.current.

division_thread = Thread.new do
  Thread.current.abort_on_exception = true

  puts "Calculating 4/0 in division_thread"
  puts "Result is: #{4/0}"
  puts "Exiting from division_thread"
end

sleep 1

puts "In the main thread"
$ RBENV_VERSION=2.4.0 ruby thread_example_2.rb

Calculating 4/0 in division_thread

thread_example_2.rb:5:in `/': divided by 0 (ZeroDivisionError)
  from thread_example_2.rb:5:in `block in <main>'

As we can see once an exception was encountered in the thread then processing stopped on both in the thread and in the main thread.

Note that Thread.current.abort_on_exception = true activates this behavior only for the current thread.

If we want this behavior globally for all the threads then we need to use Thread.abort_on_exception = true.

Running program with debug flag to stop on failure

Let’s run the original code with --debug option.

$ RBENV_VERSION=2.4.0 ruby --debug thread_example_1.rb

thread_example_1.rb:1: warning: assigned but unused variable - division_thread

Calculating 4/0 in division_thread

Exception `ZeroDivisionError' at thread_example_1.rb:3 - divided by 0
Exception `ZeroDivisionError' at thread_example_1.rb:7 - divided by 0
thread_example_1.rb:3:in `/': divided by 0 (ZeroDivisionError)
  from thread_example_1.rb:3:in `block in <main>'

In this case the exception is printed in detail and the code in main thread was not excecuted.

Usually when we execute a program with --debug option then the behavior of the program does not change. We expect the program to print more stuff but we do not expect behavior to change. However in this case the --debug option changes the behavior of the program.

Running program with join on thread to stop on failure

If a thread raises an exception and abort_on_exception and $DEBUG flags are not set then that exception will be processed at the time of joining of the thread.

division_thread = Thread.new do
  puts "Calculating 4/0 in division_thread"
  puts "Result is: #{4/0}"
  puts "Exiting from division_thread"
end

division_thread.join

puts "In the main thread"
$ RBENV_VERSION=2.4.0 ruby thread_example_3.rb

Calculating 4/0 in division_thread

thread_example_3.rb:3:in `/': divided by 0 (ZeroDivisionError)
  from thread_example_3.rb:3:in `block in <main>'

Both Thread#join and Thread#value will stop processing in the thread and in the main thread once an exception is encountered.

Introduction of report_on_exception in Ruby 2.4

Almost 6 years ago, Charles Nutter (headius) had proposed that the exceptions raised in threads should be automatically logged and reported, by default. To make his point, he explained issues similar to what we discussed above about the Ruby’s behavior of silently ignoring exceptions in threads. Here is the relevant discussion on his proposal.

Following are some of the notable points discussed.

  • Enabling Thread[.#]abort_on_exception, by default, is not always a good idea.
  • There should be a flag which, if enabled, would print the thread-killing exception info.
  • In many cases, people spawn one-off threads which are not hard-referenced using Thread#join or Thread#value. Such threads gets garbage collected. Should it report the thread-killing exception at the time of garbage collection if such a flag is enabled?
  • Should it warn using Warning#warn or redirect to STDERR device while reporting?

Charles Nutter suggested that a configurable global flag Thread.report_on_exception and instance-level flag Thread#report_on_exception should be implemented having its default value as true. When set to true, it should report print exception information.

Matz and other core members approved that Thread[.#]report_on_exception can be implemented having its default value set to false.

Charles Nutter, Benoit Daloze and other people demanded that it should be true by default so that programmers can be aware of the silently disappearing threads because of exceptions.

Shyouhei Urabe advised that due to some technical challenges, the default value should be set to false so as this feature could land in Ruby. Once this feature is in then the default value can be changed in a later release.

Nobuyoshi Nakada (nobu) pushed an implementation for Thread[.#]report_on_exception with a default value set to false. It was released in Ruby 2.4.0.

Let’s try enabling report_on_exception globally using Thread.report_on_exception.

Thread.report_on_exception = true

division_thread = Thread.new do
  puts "Calculating 4/0 in division_thread"
  puts "Result is: #{4/0}"
  puts "Exiting from division_thread"
end

addition_thread = Thread.new do
  puts "Calculating nil+4 in addition_thread"
  puts "Result is: #{nil+4}"
  puts "Exiting from addition_thread"
end

sleep 1

puts "In the main thread"
$ RBENV_VERSION=2.4.0 ruby thread_example_4.rb

Calculating 4/0 in division_thread

#<Thread:0x007fb10f018200@thread_example_4.rb:3 run> terminated with exception:
thread_example_4.rb:5:in `/': divided by 0 (ZeroDivisionError)
  from thread_example_4.rb:5:in `block in <main>'

Calculating nil+4 in addition_thread

#<Thread:0x007fb10f01aca8@thread_example_4.rb:9 run> terminated with exception:
thread_example_4.rb:11:in `block in <main>': undefined method `+' for nil:NilClass (NoMethodError)

In the main thread

It now reports the exceptions in all threads. It prints that the Thread:0x007fb10f018200 was terminated with exception: divided by 0 (ZeroDivisionError). Similarly, another thread Thread:0x007fb10f01aca8 was terminated with exception: undefined method '+' for nil:NilClass (NoMethodError).

Instead of enabling it globally for all threads, we can enable it for a particular thread using instance-level Thread#report_on_exception.

division_thread = Thread.new do
  puts "Calculating 4/0 in division_thread"
  puts "Result is: #{4/0}"
  puts "Exiting from division_thread"
end

addition_thread = Thread.new do
  Thread.current.report_on_exception = true

  puts "Calculating nil+4 in addition_thread"
  puts "Result is: #{nil+4}"
  puts "Exiting from addition_thread"
end

sleep 1

puts "In the main thread"

In the above case we have enabled report_on_exception flag just for addition_thread.

Let’s execute it.

$ RBENV_VERSION=2.4.0 ruby thread_example_5.rb

Calculating 4/0 in division_thread

Calculating nil+4 in addition_thread

#<Thread:0x007f8e6b007f70@thread_example_5.rb:7 run> terminated with exception:
thread_example_5.rb:11:in `block in <main>': undefined method `+' for nil:NilClass (NoMethodError)

In the main thread

Notice how it didn’t report the exception which killed thread division_thread. As expected, it reported the exception that killed thread addition_thread.

With the above changes ruby reports the exception as soon as it encounters. However if these threads are joined then they will still raise exception.

division_thread = Thread.new do
  Thread.current.report_on_exception = true

  puts "Calculating 4/0 in division_thread"
  puts "Result is: #{4/0}"
  puts "Exiting from division_thread"
end

begin
  division_thread.join
rescue => exception
  puts "Explicitly caught - #{exception.class}: #{exception.message}"
end

puts "In the main thread"
$ RBENV_VERSION=2.4.0 ruby thread_example_6.rb

Calculating 4/0 in division_thread

#<Thread:0x007f969d00d828@thread_example_6.rb:1 run> terminated with exception:
thread_example_6.rb:5:in `/': divided by 0 (ZeroDivisionError)
  from thread_example_6.rb:5:in `block in <main>'

Explicitly caught - ZeroDivisionError: divided by 0

In the main thread

See how we were still be able to handle the exception raised in division_thread above after joining it despite it reported it before due to Thread#report_on_exception flag.

report_on_exception defaults to true in Ruby 2.5

Benoit Daloze (eregon) strongly advocated that both the Thread.report_on_exception and Thread#report_on_exception should have default value as true. Here is the relevant feature request.

After approval from Matz, Benoit Daloze pushed the implementation by fixing the failing tests and silencing the unnecessary verbose warnings.

It was released as part of Ruby 2.5.

Now in ruby 2.5 we can simply write like this.

division_thread = Thread.new do
  puts "Calculating 4/0 in division_thread"
  puts "Result is: #{4/0}"
  puts "Exiting from division_thread"
end

addition_thread = Thread.new do
  puts "Calculating nil+4 in addition_thread"
  puts "Result is: #{nil+4}"
  puts "Exiting from addition_thread"
end

sleep 1

puts "In the main thread"

Let’s execute it with Ruby 2.5.

$ RBENV_VERSION=2.5.0 ruby thread_example_7.rb

Calculating 4/0 in division_thread

#<Thread:0x00007f827689a238@thread_example_7.rb:1 run> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
  1: from thread_example_7.rb:3:in `block in <main>'
thread_example_7.rb:3:in `/': divided by 0 (ZeroDivisionError)

Calculating nil+4 in addition_thread

#<Thread:0x00007f8276899b58@thread_example_7.rb:7 run> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
thread_example_7.rb:9:in `block in <main>': undefined method `+' for nil:NilClass (NoMethodError)

In the main thread

We can disable the thread exception reporting globally using Thread.report_on_exception = false or for a particular thread using Thread.current.report_on_exception = false.

Future Possibilities

In addition to this feature, Charles Nutter also suggested that it will be good if there exists a callback handler which can accept a block to be executed when a thread dies due to an exception. The callback handler can be at global level or it can be for a specific thread.

Thread.on_exception do
  # some stuff
end

In the absence of such handler libraries need to resort to custom code to handle exceptions. Here is how Sidekiq handles exceptions raised in threads.

Important thing to note is that report_on_exception does not change behavior of the code. It does more reporting when a thread dies and when it comes to thread dies more reporting is a good thing.

Rails 5.2 added Date#prev_occurring and Date#next_occurring to return specified next & previous occurring day of week

This blog is part of our Rails 5.2 series.

Before Rails 5.2, this is how we would write to find next or previous occurring day of the week.

Assume that current date is Tue, 27 Feb 2018.

# find previous thursday
>> Date.yesterday.beginning_of_week(:thursday)
=> Thu, 22 Feb 2018

# find next thursday
>> Date.tomorrow.end_of_week(:friday)
=> Thu, 01 Mar 2018

Rails 5.2 has introduced methods Date#prev_occurring and Date#next_occurring to find next & previous occurring day of the week.

# find previous thursday
>> Date.prev_occurring(:thursday)
=> Thu, 22 Feb 2018

# find next thursday
>> Date.next_occurring(:thursday)
=> Thu, 01 Mar 2018

Ruby 2.5 supports measuring branch and method coverages

Ruby comes with Coverage, a simple standard library for test coverage measurement for a long time.

Before Ruby 2.5

Before Ruby 2.5, we could measure just the line coverage using Coverage.

Line coverage tells us whether a line is executed or not. If executed, then how many times that line was executed.

We have a file called score.rb.

score = 33

if score >= 40
  p :PASSED
else
  p :FAILED
end

Now create another file score_coverage.rb.

require "coverage"

Coverage.start
load "score.rb"
p Coverage.result

We used Coverage#start method to measure the coverage of score.rb file. Coverage#result returns the coverage result.

Let’s run it with Ruby 2.4.

$ RBENV_VERSION=2.4.0 ruby score_coverage.rb
:FAILED
{ "score.rb"=> [1, nil, 1, 0, nil, 1, nil] }

Let’s look at the output. Each value in the array [1, nil, 1, 0, nil, 1, nil] denotes the count of line executions by the interpreter for each line in score.rb file.

This array is also called the “line coverage” of score.rb file.

A nil value in line coverage array means coverage is disabled for that particular line number or it is not a relevant line. Lines like else, end and blank lines have line coverage disabled.

Here’s how we can read above line coverage result.

  • Line number 1 (i.e. 0th index in the above result array) was executed once.
  • Coverage was disabled for line number 2 (i.e. index 1) as it is blank.
  • Line number 3 (i.e. index 2) was executed once.
  • Line number 4 did not execute.
  • Coverage was disabled for line number 5 as it contains only else clause.
  • Line number 6 was executed once.
  • Coverage was disabled for line number 7 as it contains just end keyword.

After Ruby 2.5

There was a pull request opened in 2014 to add method coverage and decision coverage metrics in Ruby. It was rejected by Yusuke Endoh as he saw some issues with it and mentioned that he was also working on a similar implementation.

In Ruby 2.5, Yusuke Endoh added branch coverage and method coverage feature to the Coverage library.

Let’s see what’s changed in Coverage library in Ruby 2.5.

Line Coverage

If we execute above example using Ruby 2.5, we will see no change in the result.

$ RBENV_VERSION=2.5.0 ruby score_coverage.rb
:FAILED
{ "score.rb" => [1, nil, 1, 0, nil, 1, nil] }

This behavior is maintained to ensure that the Coverage#start API stays 100% backward compatible.

If we explicitly enable lines option on Coverage#start method in the above score_coverage.rb file, the coverage result will be different now.

require "coverage"

Coverage.start(lines: true)
load "score.rb"
p Coverage.result
$ RBENV_VERSION=2.5.0 ruby score_coverage.rb
:FAILED
{ "score.rb" => {
    :lines => [1, nil, 1, 0, nil, 1, nil]
  }
}

We can see that the coverage result is now a hash which reads that the score.rb file has lines coverage as [1, nil, 1, 0, nil, 1, nil].

Branch Coverage

Branch coverage helps us identify which branches are executed and which ones are not executed.

Let’s see how to get branch coverage.

We will update the score_coverage.rb by enabling branches option.

require "coverage"

Coverage.start(branches: true)
load "score.rb"
p Coverage.result
$ RBENV_VERSION=2.5.0 ruby score_coverage.rb
:FAILED
{ "score.rb" =>
  { :branches => {
      [:if, 0, 3, 0, 7, 3] => {
        [:then, 1, 4, 2, 4, 15] => 0,
        [:else, 2, 6, 2, 6, 15] => 1
      }
    }
  }
}

Here is how to read the data in array.

[
  BRANCH_TYPE,
  UNIQUE_ID,
  START_LINE_NUMBER,
  START_COLUMN_NUMBER,
  END_LINE_NUMBER,
  END_COLUMN_NUMBER
]

Please note that column numbers start from 0 and line numbers start from 1.

Let’s try to read above printed branch coverage result.

[:if, 0, 3, 0, 7, 3] reads that if statement starts at line 3 & column 0 and ends at line 7 & column 3.

[:then, 1, 4, 2, 4, 15] reads that then clause starts at line 4 & column 2 and ends at line 4 & column 15.

Similarly, [:else, 2, 6, 2, 6, 15] reads that else clause starts at line 6 & column 2 and ends at line 6 & column 15.

Most importantly as per the branch coverage format, we can see that the branch from if to then was never executed since COUNTER is 0. The another branch from if to else was executed once since COUNTER is 1.

Method Coverage

Measuring method coverage helps us identify which methods were invoked and which were not.

We have a file grade_calculator.rb.

students_scores = { "Sam" => [53, 91, 72],
                    "Anna" => [91, 97, 95],
                    "Bob" => [33, 69, 63] }

def average(scores)
  scores.reduce(&:+)/scores.size
end

def grade(average_score)
  case average_score
  when 90.0..100.0 then :A
  when 80.0..90.0 then :B
  when 70.0..80.0 then :C
  when 60.0..70.0 then :D
  else :F
  end
end

def greet
  puts "Congratulations!"
end

def warn
  puts "Try hard next time!"
end


students_scores.each do |student_name, scores|
  achieved_grade = grade(average(scores))

  puts "#{student_name}, you've got '#{achieved_grade}' grade."

  if achieved_grade == :A
    greet
  elsif achieved_grade == :F
    warn
  end

  puts
end

To measure method coverage of above file, let’s create grade_calculator_coverage.rb by enabling methods option on Converage#start method.

require "coverage"

Coverage.start(methods: true)
load "grade_calculator.rb"
p Coverage.result

Let’s run it using Ruby 2.5.

$ RBENV_VERSION=2.5.0 ruby grade_calculator_coverage.rb
Sam, you've got 'C' grade.

Anna, you've got 'A' grade.
Congratulations!

Bob, you've got 'F' grade.
Try hard next time!

{ "grade_calculator.rb" => {
    :methods => {
      [Object, :warn, 23, 0, 25, 3] => 1,
      [Object, :greet, 19, 0, 21, 3] => 1,
      [Object, :grade, 9, 0, 17, 3] => 3,
      [Object, :average, 5, 0, 7, 3] => 3
    }
  }
}

The format of method coverage result is defined as shown below.

[ CLASS_NAME,
  METHOD_NAME,
  START_LINE_NUMBER,
  START_COLUMN_NUMBER,
  END_LINE_NUMBER,
  END_COLUMN_NUMBER ]

Therefore, [Object, :grade, 9, 0, 17, 3] => 3 reads that the Object#grade method which starts from line 9 & column 0 to line 17 & column 3 was invoked 3 times.

Conclusion

We can measure all coverages at once also.

Coverage.start(lines: true, branches: true, methods: true)

What’s the use of these different types of coverages anyway?

Well, one use case is to integrate this in a test suite and to determine which lines, branches and methods are executed and which ones are not executed by the test. Further, we can sum up these and evaluate total coverage of a test suite.

Author of this feature, Yusuke Endoh (mame) has released coverage-helpers gem which allows further advanced manipulation and processing of coverage results obtained using Coverage#result.

Gpg decryption without pin entry pop up using GPGME

In one of our projects, we implemented GPG decryption.

What is GPG ?

GPG is a complete and free implementation of the OpenPGP standard as defined by RFC4880 (also known as PGP).

We used GPGME gem for this purpose. It provides three levels of API. In our case, we used Crypto which has the high level convenience methods to encrypt, decrypt, sign and verify signatures.

We needed to import private key for decrypting a file that was encrypted using paired public key. First let’s import the required private key.

GPGME::Key.import File.open('certs/pgp.key')

Let’s decrypt the file.

crypto = GPGME::Crypto.new
options = { output: File.open('file.csv', 'wb') }

crypto.decrypt File.open('file.csv.gpg'), options

Above code has one problem. It will open a pop up for password input that has been used when public and private keys have been generated.

To support password input without pop up, we updated the code as below.

crypto = GPGME::Crypto.new
options = {
            output: File.open('file.csv', 'wb'),
            pinentry_mode: GPGME::PINENTRY_MODE_LOOPBACK,
            password: 'welcome'
          }

crypto.decrypt File.open('file.csv.gpg'), options

Here, pinentry_mode option allows password input without pop up.

We did not use latest version of GPG since it does not support pinentry_mode option. Instead, We used 2.1.20 version which has support for this option. Here is the build instruction for that.

Practical usage of identity function

If you are learning functional programming then you can’t go far without running into “identity function”.

An identity function is a very basic function that

  • takes one argument
  • returns the argument
f(x) = x;

This seems like the most useless function in the world. We never needed any function like this while building any application. Then what’s the big deal about this identity function.

In this blog we will see how this identity concept is used in the real world.

For the implementation we will be using Ramda.js. We previously wrote about how we, at BigBinary, write JavaScript code using Ramda.js.

Again please note that in the following code R stands for Ramda and not for programming language R.

Example 1

Here is JavaScript code.

if (x) return x;
return [];

Here is same code using Ramda.js.

R.ifElse(
  R.isNil,
  () => [],
  R.identity
);

try it

Example 2

Here we will use identity as the return value in the default case.

R.cond([
  [R.equals(0), R.always("0")],
  [R.equals(10), R.always("10")],
  [R.T, R.identity]
]);

try it

Example 3

Get the unique items from the list.

R.uniqBy(R.identity, [1,1,2])

try it

Example 4

Count occurrences of items in the list.

R.countBy(R.identity, ["a","a","b","c","c","c"]);

try it

Example 5

Begin value from zero all the way to n-1.

R.times(R.identity, 5)

try it

Ruby 2.5 adds Exception#full_message method

This blog is part of our Ruby 2.5 series. Ruby 2.5 was recently released.

Before Ruby 2.5, if we want to log a caught exception, we would need to format it ourselves.

class AverageService
  attr_reader :numbers, :coerced_numbers

  def initialize(numbers)
    @numbers = numbers
    @coerced_numbers = coerce_numbers
  end

  def average
    sum / count
  end

  private

  def coerce_numbers
    numbers.map do |number|
      begin
        Float(number)
      rescue Exception => exception
        puts "#{exception.message} (#{exception.class})\n\t#{exception.backtrace.join("\n\t")}"
        puts "Coercing '#{number}' as 0.0\n\n"

        0.0
      end
    end
  end

  def sum
    coerced_numbers.map(&:to_f).sum
  end

  def count
    coerced_numbers.size.to_f
  end
end

average = AverageService.new(ARGV).average
puts "Average is: #{average}"
$ RBENV_VERSION=2.4.0 ruby average_service.rb 5 4f 7 1s0
invalid value for Float(): "4f" (ArgumentError)
	average_service.rb:18:in `Float'
	average_service.rb:18:in `block in coerce_numbers'
	average_service.rb:16:in `map'
	average_service.rb:16:in `coerce_numbers'
	average_service.rb:6:in `initialize'
	average_service.rb:37:in `new'
	average_service.rb:37:in `<main>'

Coercing '4f' as 0.0

invalid value for Float(): "1s0" (ArgumentError)
	average_service.rb:18:in `Float'
	average_service.rb:18:in `block in coerce_numbers'
	average_service.rb:16:in `map'
	average_service.rb:16:in `coerce_numbers'
	average_service.rb:6:in `initialize'
	average_service.rb:37:in `new'
	average_service.rb:37:in `<main>'

Coercing '1s0' as 0.0

Average of [5.0, 0.0, 7.0, 0.0] is: 3.0

It was proposed that there should be a simple method to print the caught exception using the same format that ruby uses while printing an uncaught exception.

Some of the proposed method names were display, formatted, to_formatted_s, long_message, and full_message.

Matz approved the Exception#full_message method name.

In Ruby 2.5, we can re-write above example as follows.

class AverageService
  attr_reader :numbers, :coerced_numbers

  def initialize(numbers)
    @numbers = numbers
    @coerced_numbers = coerce_numbers
  end

  def average
    sum / count
  end

  private

  def coerce_numbers
    numbers.map do |number|
      begin
        Float(number)
      rescue Exception => exception
        puts exception.full_message
        puts "Coercing '#{number}' as 0.0\n\n"

        0.0
      end
    end
  end

  def sum
    coerced_numbers.map(&:to_f).sum
  end

  def count
    coerced_numbers.size.to_f
  end
end

average = AverageService.new(ARGV).average
puts "Average is: #{average}"
$ RBENV_VERSION=2.5.0 ruby average_service.rb 5 4f 7 1s0
Traceback (most recent call last):
	6: from average_service.rb:37:in `<main>'
	5: from average_service.rb:37:in `new'
	4: from average_service.rb:6:in `initialize'
	3: from average_service.rb:16:in `coerce_numbers'
	2: from average_service.rb:16:in `map'
	1: from average_service.rb:18:in `block in coerce_numbers'
average_service.rb:18:in `Float': invalid value for Float(): "4f" (ArgumentError)

Coercing '4f' as 0.0

Traceback (most recent call last):
	6: from average_service.rb:37:in `<main>'
	5: from average_service.rb:37:in `new'
	4: from average_service.rb:6:in `initialize'
	3: from average_service.rb:16:in `coerce_numbers'
	2: from average_service.rb:16:in `map'
	1: from average_service.rb:18:in `block in coerce_numbers'
average_service.rb:18:in `Float': invalid value for Float(): "1s0" (ArgumentError)

Coercing '1s0' as 0.0

Average of [5.0, 0.0, 7.0, 0.0] is: 3.0

Note that, Ruby 2.5 prints exception backtrace in reverse order if STDERR is unchanged and is a TTY as discussed in our previous blog post.

Ruby 2.5 prints backtrace and error message in reverse order

This blog is part of our Ruby 2.5 series. Ruby 2.5 was recently released.

Stack trace or backtrace is a sequential representation of the stack of method calls in a program which gets printed when an exception is raised. It is often used to find out the exact location in a program from where the exception was raised.

Before Ruby 2.5

Before Ruby 2.5, the printed backtrace contained the exception class and the error message at the top. Next line contained where in the program the exception was raised. Next we got more lines which contained cascaded method calls.

Consider a simple Ruby program.

class DivisionService
  attr_reader :a, :b

  def initialize(a, b)
    @a, @b = a.to_i, b.to_i
  end

  def divide
    puts a / b
  end
end

DivisionService.new(ARGV[0], ARGV[1]).divide

Let’s execute it using Ruby 2.4.

$ RBENV_VERSION=2.4.0 ruby division_service.rb 5 0

division_service.rb:9:in `/': divided by 0 (ZeroDivisionError)
	from division_service.rb:9:in `divide'
	from division_service.rb:13:in `<main>'

In the printed backtrace above, the first line shows the location, error message and the exception class name; whereas the subsequent lines shows the caller method names and their locations. Each line in the backtrace above is often considered as a stack frame placed on the call stack.

Most of the times, a backtrace has so many lines that it makes it very difficult to fit the whole backtrace in the visible viewport of the terminal.

Since the backtrace is printed in top to bottom order the meaningful information like error message, exception class and the exact location where the exception was raised is displayed at top of the backtrace. It means developers often need to scroll to the top in the terminal window to find out what went wrong.

After Ruby 2.5

Over 4 years ago an issue was created to make printing of backtrace in reverse order configurable.

After much discussion Nobuyoshi Nakada made the commit to print backtrace and error message in reverse order only when the error output device (STDERR) is a TTY (i.e. a terminal). Message will not be printed in reverse order if the original STDERR is attached to something like a File object.

Look at the code here where the check happens if STDERR is a TTY and is unchanged.

Let’s execute the same program using Ruby 2.5.

$ RBENV_VERSION=2.5.0 ruby division_service.rb 5 0

Traceback (most recent call last):
	2: from division_service.rb:13:in `<main>'
	1: from division_service.rb:9:in `divide'
division_service.rb:9:in `/': divided by 0 (ZeroDivisionError)

$

We can notice two new changes in the above backtrace.

  1. The error message and exception class is printed last (i.e. at the bottom).
  2. The stack also adds frame number when printing in reverse order.

This feature makes the debugging convenient when the backtrace size is a quite big and cannot fit in the terminal window. We can easily see the error message without scrolling up now.

Note that, the Exception#backtrace attribute still holds an array of stack frames like before in the top to bottom order.

So if we rescue the caught exception and print the backtrace manually

class DivisionService
  attr_reader :a, :b

  def initialize(a, b)
    @a, @b = a.to_i, b.to_i
  end

  def divide
    puts a / b
  end
end

begin
  DivisionService.new(ARGV[0], ARGV[1]).divide
rescue Exception => e
  puts "#{e.class}: #{e.message}"
  puts e.backtrace.join("\n")
end

we will get the old behavior.

$ RBENV_VERSION=2.5.0 ruby division_service.rb 5 0

ZeroDivisionError: divided by 0
division_service.rb:9:in `/'
division_service.rb:9:in `divide'
division_service.rb:16:in `<main>'

$

Also, note that if we assign STDERR with a File object, thus making it a non-TTY

puts "STDERR is a TTY? [before]: #{$stderr.tty?}"
$stderr = File.new("stderr.log", "w")
$stderr.sync = true
puts "STDERR is a TTY? [after]: #{$stderr.tty?}"

class DivisionService
  attr_reader :a, :b

  def initialize(a, b)
    @a, @b = a.to_i, b.to_i
  end

  def divide
    puts a / b
  end
end

DivisionService.new(ARGV[0], ARGV[1]).divide

we can get the old behavior but the backtrace would be written to the specified file and not to STDERR.

$ RBENV_VERSION=2.5.0 ruby division_service.rb 5 0

STDERR is a TTY? [before]: true
STDERR is a TTY? [after]: false

$ cat stderr.log

division_service.rb:14:in `/': divided by 0 (ZeroDivisionError)
	from division_service.rb:14:in `divide'
	from division_service.rb:18:in `<main>'

$

This feature has been tagged as experimental feature. What it means is that Ruby team is gathering feedback on this feature.

Deploying Ruby on Rails application using HAProxy Ingress with unicorn/puma and websockets

After months of testing we recently moved a Ruby on Rails application to production that is using Kubernetes cluster.

In this article we will discuss how to setup path based routing for a Ruby on Rails application in kubernetes using HAProxy ingress.

This post assumes that you have basic understanding of Kubernetes terms like pods, deployments, services, configmap and ingress.

Typically our Rails app has services like unicorn/puma, sidekiq/delayed-job/resque, Websockets and some dedicated API services. We had one web service exposed to the world using load balancer and it was working well. But as the traffic increased it became necessary to route traffic based on URLs/path.

However Kubernetes does not supports this type of load balancing out of the box. There is work in progress for alb-ingress-controller to support this but we could not rely on it for production usage as it is still in alpha.

The best way to achieve path based routing was to use ingress controller.

We researched and found that there are different types of ingress available in k8s world.

  1. nginx-ingress
  2. ingress-gce
  3. HAProxy-ingress
  4. traefik
  5. voyager

We experimented with nginx-ingress and HAProxy and decided to go with HAProxy. HAProxy has better support for Rails websockets which we needed in the project.

We will walk you through step by step on how to use haproxy ingress in a Rails app.

Configuring Rails app with HAProxy ingress controller

Here is what we are going to do.

  • Create a Rails app with different services and deployments.
  • Create tls secret for SSL.
  • Create HAProxy ingress configmap.
  • Create HAProxy ingress controller.
  • Expose ingress with service type LoadBalancer
  • Setup app DNS with ingress service.
  • Create different ingress rules specifying path based routing.
  • Test the path based routing.

Now let’s build Rails application deployment manifest for services like web(unicorn),background(sidekiq), Websocket(ruby thin),API(dedicated unicorn).

Here is our web app deployment and service template.

---
apiVersion: v1
kind: Deployment
metadata:
  name: test-production-web
  labels:
    app: test-production-web
  namespace: test
spec:
  template:
    metadata:
      labels:
        app: test-production-web
    spec:
      containers:
      - image: <your-repo>/<your-image-name>:latest
        name: test-production
        imagePullPolicy: Always
       env:
        - name: POSTGRES_HOST
          value: test-production-postgres
        - name: REDIS_HOST
          value: test-production-redis
        - name: APP_ENV
          value: production
        - name: APP_TYPE
          value: web
        - name: CLIENT
          value: test
        ports:
        - containerPort: 80
      imagePullSecrets:
        - name: registrykey
---
apiVersion: v1
kind: Service
metadata:
  name: test-production-web
  labels:
    app: test-production-web
  namespace: test
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: test-production-web

Here is background app deployment and service template.

---
apiVersion: v1
kind: Deployment
metadata:
  name: test-production-background
  labels:
    app: test-production-background
  namespace: test
spec:
  template:
    metadata:
      labels:
        app: test-production-background
    spec:
      containers:
      - image: <your-repo>/<your-image-name>:latest
        name: test-production
        imagePullPolicy: Always
       env:
        - name: POSTGRES_HOST
          value: test-production-postgres
        - name: REDIS_HOST
          value: test-production-redis
        - name: APP_ENV
          value: production
        - name: APP_TYPE
          value: background
        - name: CLIENT
          value: test
        ports:
        - containerPort: 80
      imagePullSecrets:
        - name: registrykey
---
apiVersion: v1
kind: Service
metadata:
  name: test-production-background
  labels:
    app: test-production-background
  namespace: test
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: test-production-background

Here is websocket app deployment and service template.

---
apiVersion: v1
kind: Deployment
metadata:
  name: test-production-websocket
  labels:
    app: test-production-websocket
  namespace: test
spec:
  template:
    metadata:
      labels:
        app: test-production-websocket
    spec:
      containers:
      - image: <your-repo>/<your-image-name>:latest
        name: test-production
        imagePullPolicy: Always
       env:
        - name: POSTGRES_HOST
          value: test-production-postgres
        - name: REDIS_HOST
          value: test-production-redis
        - name: APP_ENV
          value: production
        - name: APP_TYPE
          value: websocket
        - name: CLIENT
          value: test
        ports:
        - containerPort: 80
      imagePullSecrets:
        - name: registrykey
---
apiVersion: v1
kind: Service
metadata:
  name: test-production-websocket
  labels:
    app: test-production-websocket
  namespace: test
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: test-production-websocket

Here is API app deployment and service info.

---
apiVersion: v1
kind: Deployment
metadata:
  name: test-production-api
  labels:
    app: test-production-api
  namespace: test
spec:
  template:
    metadata:
      labels:
        app: test-production-api
    spec:
      containers:
      - image: <your-repo>/<your-image-name>:latest
        name: test-production
        imagePullPolicy: Always
       env:
        - name: POSTGRES_HOST
          value: test-production-postgres
        - name: REDIS_HOST
          value: test-production-redis
        - name: APP_ENV
          value: production
        - name: APP_TYPE
          value: api
        - name: CLIENT
          value: test
        ports:
        - containerPort: 80
      imagePullSecrets:
        - name: registrykey
---
apiVersion: v1
kind: Service
metadata:
  name: test-production-api
  labels:
    app: test-production-api
  namespace: test
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: test-production-api

Let’s launch this manifest using kubectl apply.

$ kubectl apply -f test-web.yml -f test-background.yml -f test-websocket.yml -f test-api.yml
deployment "test-production-web" created
service "test-production-web" created
deployment "test-production-background" created
service "test-production-background" created
deployment "test-production-websocket" created
service "test-production-websocket" created
deployment "test-production-api" created
service "test-production-api" created

Once our app is deployed and running we should create HAProxy ingress. Before that let’s create a tls secret with our SSL key and certificate.

This is also used to enable HTTPS for app URL and to terminate it on L7.

$ kubectl create secret tls tls-certificate --key server.key --cert server.pem

Here server.key is our SSL key and server.pem is our SSL certificate in pem format.

Now let’s Create HAProxy controller resources.

HAProxy configmap

For all the available configuration parameters from HAProxy refer here.

apiVersion: v1
data:
    dynamic-scaling: "true"
    backend-server-slots-increment: "4"
kind: ConfigMap
metadata:
  name: haproxy-configmap
  namespace: test

HAProxy Ingress controller deployment

Deployment template for the Ingress controller with at-least 2 replicas to manage rolling deploys.

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    run: haproxy-ingress
  name: haproxy-ingress
  namespace: test
spec:
  replicas: 2
  selector:
    matchLabels:
      run: haproxy-ingress
  template:
    metadata:
      labels:
        run: haproxy-ingress
    spec:
      containers:
      - name: haproxy-ingress
        image: quay.io/jcmoraisjr/haproxy-ingress:v0.5-beta.1
        args:
        - --default-backend-service=$(POD_NAMESPACE)/test-production-web
        - --default-ssl-certificate=$(POD_NAMESPACE)/tls-certificate
        - --configmap=$(POD_NAMESPACE)/haproxy-configmap
        - --ingress-class=haproxy
        ports:
        - name: http
          containerPort: 80
        - name: https
          containerPort: 443
        - name: stat
          containerPort: 1936
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace

Notable fields in above manifest are arguments passed to controller.

--default-backend-service is the service when No rule is matched your request will be served by this app.

In our case it is test-production-web service, But it can be custom 404 page or whatever better you think.

--default-ssl-certificate is the SSL secret we just created above this will terminate SSL on L7 and our app is served on HTTPS to outside world.

HAProxy Ingress service

This is the LoadBalancer type service to allow client traffic to reach our Ingress Controller.

LoadBalancer has access to both public network and internal Kubernetes network while retaining the L7 routing of the Ingress Controller.

apiVersion: v1
kind: Service
metadata:
  labels:
    run: haproxy-ingress
  name: haproxy-ingress
  namespace: test
spec:
  type: LoadBalancer
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  - name: https
    port: 443
    protocol: TCP
    targetPort: 443
  - name: stat
    port: 1936
    protocol: TCP
    targetPort: 1936
  selector:
    run: haproxy-ingress

Now let’s apply all the manifests of HAProxy.

$ kubectl apply -f haproxy-configmap.yml -f haproxy-deployment.yml -f haproxy-service.yml
configmap "haproxy-configmap" created
deployment "haproxy-ingress" created
service "haproxy-ingress" created

Once all the resources are running get the LoadBalancer endpoint using.

$ kubectl -n test get svc haproxy-ingress -o wide

NAME               TYPE           CLUSTER-IP       EXTERNAL-IP                                                            PORT(S)                                     AGE       SELECTOR
haproxy-ingress   LoadBalancer   100.67.194.186   a694abcdefghi11e8bc3b0af2eb5c5d8-806901662.us-east-1.elb.amazonaws.com   80:31788/TCP,443:32274/TCP,1936:32157/TCP   2m        run=ingress

DNS mapping with application URL

Once we have ELB endpoint of ingress service, map the DNS with URL like test-rails-app.com.

Ingress Implementation

Now after doing all the hard work it is time to configure ingress and path based rules.

In our case we want to have following rules.

https://test-rails-app.com requests to be served by test-production-web.

https://test-rails-app.com/websocket requests to be served by test-production-websocket.

https://test-rails-app.com/api requests to be served by test-production-api.

Let’s create a ingress manifest defining all the rules.

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress
  namespace: test
spec:
  tls:
    - hosts:
      - test-rails-app.com
      secretName: tls-certificate
  rules:
    - host: test-rails-app.com
      http:
        paths:
          - path: /
            backend:
              serviceName: test-production-web
              servicePort: 80
          - path: /api
            backend:
              serviceName: test-production-api
              servicePort: 80
          - path: /websocket
            backend:
              serviceName: test-production-websocket
              servicePort: 80

Moreover there are Ingress Annotations for adjusting configuration changes.

As expected,now our default traffic on / is routed to test-production-web service.

/api is routed to test-production-api service.

/websocket is routed to test-production-websocket service.

Thus ingress implementation solves our purpose of path based routing and terminating SSL on L7 on Kubernetes.

Rails 5.2 adds default option to module and class attribute accessors

This blog is part of our Rails 5.2 series.

When DHH introduced support for specifying a default value for class_attribute, Genadi Samokovarov brought to notice that the module and class attribute accessor macros also support specifying a default value but using a block and not with a default option.

To have consistent and symmetrical behaviour across all the attribute extensions, it was decided to support specifying a default value using default option for all the module and class attribute macros as well.

mattr_accessor, mattr_reader and mattr_writer macros generate getter and setter methods at the module level.

Similarly, cattr_accessor, cattr_reader, and cattr_writer macros generate getter and setter methods at the class level.

Before Rails 5.2

Before Rails 5.2, this is how we would set the default values for the module and class attribute accessor macros.

module ActivityLoggerHelper
  mattr_accessor :colorize_logs
  mattr_writer :log_ip { false }

  self.colorize_logs = true
end

class ActivityLogger
  include ActivityLoggerHelper

  cattr_writer :logger { Logger.new(STDOUT) }
  cattr_accessor :level
  cattr_accessor :settings
  cattr_reader :pid { Process.pid }

  @@level = Logger::DEBUG
  self.settings = {}
end

After Rails 5.2

We can still set a default value of a module or class attribute accessor by providing a block. In this pull request, support for specifying a default value using a new default option has been introduced.

So instead of

cattr_writer :logger { Logger.new(STDOUT) }

or

cattr_writer :logger
self.logger = Logger.new(STDOUT)

or

cattr_writer :logger
@@logger = Logger.new(STDOUT)

we can now easily write

cattr_writer :logger, default: Logger.new(STDOUT)

Same applies to the other attribute accessor macros like mattr_accessor, mattr_reader, mattr_writer, cattr_accessor, and cattr_reader.

Note that, the old way of specifying a default value using the block syntax will work but will not be documented anywhere.

Also, note that if we try to set the default value by both ways i.e. by providing a block as well as by specifying a default option; the value provided by default option will always take the precedence.

mattr_accessor(:colorize_logs, default: true) { false }

Here, @@colorize_logs would be set with true as per the above precedence rule.

Here is a test which verifies this behavior.

Finally, here is simplified version using the new default option.

module ActivityLoggerHelper
  mattr_accessor :colorize_logs, default: true
  mattr_writer :log_ip, default: false
end

class ActivityLogger
  include ActivityLoggerHelper

  cattr_writer :logger, default: Logger.new(STDOUT)
  cattr_accessor :level, default: Logger::DEBUG
  cattr_accessor :settings, default: {}
  cattr_reader :pid, default: Process.pid
end

Rails 5.2 supports specifying default value for a class_attribute

This blog is part of our Rails 5.2 series.

It is very common to set a default value for a class_attribute.

Before Rails 5.2, to specify a default value for a class_attribute, we needed to write like this.

class ActivityLogger
  class_attribute :logger
  class_attribute :settings

  self.logger = Logger.new(STDOUT)
  self.settings = {}
end

As we can see above, it requires additional keystrokes to set a default value for each class_attribute.

Rails 5.2 has added support for specifying a default value for a class_attribute using default option.

class ActivityLogger
  class_attribute :logger, default: Logger.new(STDOUT)
  class_attribute :settings, default: {}
end

This enhancement was introduced in this pull request.

Ruby 2.5 added Hash#slice method

This blog is part of our Ruby 2.5 series. Ruby 2.5 was recently released.

Ruby 2.4

Let’s say that we have a hash { id: 1, name: 'Ruby 2.5', description: 'BigBinary Blog' } and we want to select key value pairs having keys name and description.

We can use Hash#select method.

irb> blog = { id: 1, name: 'Ruby 2.5', description: 'BigBinary Blog' }
  => {:id=>1, :name=>"Ruby 2.5", :description=>"BigBinary Blog"}

irb> blog.select { |key, value| [:name, :description].include?(key) }
  => {:name=>"Ruby 2.5", :description=>"BigBinary Blog"}

Matzbara Masanao proposed a simple method to take care of this.

Some of the names proposed were choice and pick.

Matz suggested the name slice since this method is ActiveSupport compatible.

Ruby 2.5.0

irb> blog = { id: 1, name: 'Ruby 2.5', description: 'BigBinary Blog' }
  => {:id=>1, :name=>"Ruby 2.5", :description=>"BigBinary Blog"}

irb> blog.slice(:name, :description)
  => {:name=>"Ruby 2.5", :description=>"BigBinary Blog"}

As we can see, now we can use a simple method slice to select key value pairs from a hash with specified keys.

Here is relevant commit and discussion.

Ruby 2.5 allows creating structs with keyword arguments

This blog is part of our Ruby 2.5 series. Ruby 2.5 was recently released.

In Ruby, structs can be created using positional arguments.

Customer = Struct.new(:name, :email)
Customer.new("John", "john@example.com")

This approach works when the arguments list is short. When arguments list increases then it gets harder to track which position maps to which value.

Here if we pass keyword argument then we won’t get any error. But the values are not what we wanted.

Customer.new(name: "John", email: "john@example.com")
=> #<struct Customer name={:name=>"John", :email=>"john@example.com"}, email=nil>

Ruby 2.5 introduced creating structs using keyword arguments. Relevant pull request is here.

However this introduces a problem. How do we indicate to Struct if we want to pass arguments using position or keywords.

Takashi Kokubun suggested to use keyword_argument as an identifier.

Customer = Struct.new(:name, :email, keyword_argument: true)
Customer.create(name: "John", email: "john@example.com")

Matz suggested to change the name to keyword_init.

So in Ruby 2.5 we can create structs using keywords as long as we are passing keyword_init.

Customer = Struct.new(:name, :email, keyword_init: true)

Customer.new(name: "John", email: "john@example.com")
=> #<struct Customer name="John", email="john@example.com">

Rails 5.2 allows mailers to use custom Active Job class

This blog is part of our Rails 5.2 series.

Rails allows sending emails asynchronously via Active Job.

Notifier.welcome(User.first).deliver_later

It uses ActionMailer::DeliveryJob as the default job class to send emails. This class is defined internally by Rails.

The DeliveryJob defines handle_exception_with_mailer_class method to handle exception and to do some housekeeping work.

def handle_exception_with_mailer_class(exception)
  if klass = mailer_class
    klass.handle_exception exception
  else
    raise exception
  end
end

One might need more control on the job class to retry the job under certain conditions or add more logging around exceptions.

Before Rails 5.2, it was not possible to use a custom job class for this purpose.

Rails 5.2 has added a feature to configure the job class per mailer.

class CustomNotifier < ApplicationMailer
  self.delivery_job = CustomNotifierDeliveryJob
end

By default, Rails will use the internal DeliveryJob class if the delivery_job configuration is not present in the mailer class.

Now, Rails will use CustomNotifierDeliveryJob for sending emails for CustomNotifier mailer.

CustomNotifier.welcome(User.first).deliver_later

As mentioned above CustomNotifierDeliveryJob can be further configured for logging, exception handling and reporting.

By default, deliver_later will pass following arguments to the perform method of the CustomNotifierDeliveryJob.

  • mailer class name
  • mailer method name
  • mail delivery method
  • original arguments with which the mail is to be sent
class CustomNotifierDeliveryJob < ApplicationJob

  rescue_from StandardError, with: :handle_exception_with_mailer_class

  retry_on CustomNotifierException

  discard_on ActiveJob::DeserializationError

  def perform(mailer, mail_method, delivery_method, *args)
    logger.log "Mail delivery started"
    klass = mailer.constantize
    klass.public_send(mail_method, *args).send(delivery_method)
    logger.log "Mail delivery completed"
  end

  def handle_exception_with_mailer_class(exception)
    if klass = mailer_class
      klass.handle_exception exception
    else
      raise exception
    end
  end
end

We can also simply inherit from the ActionMailer::DeliveryJob and override the retry logic.

class CustomNotifierDeliveryJob < ActionMailer::DeliveryJob
  retry_on CustomNotifierException
end

Rails 5.2 supports descending indexes for MySQL

This blog is part of our Rails 5.2 series.

An index is used to speed up the performance of queries on a database.

Rails allows us to create index on a database column by means of a migration. By default, the sort order for the index is ascending.

But consider the case where we are fetching reports from the database. And while querying the database, we always want to get the latest report. In this case, it is efficient to specify the sort order for the index to be descending.

We can specify the sort order by adding an index to the required column by adding a migration .

add_index :reports, [:user_id, :name], order: { user_id: :asc, name: :desc }

PostgreSQL

If our Rails application is using postgres database, after running the above migration we can verify that the sort order was added in schema.rb

create_table "reports", force: :cascade do |t|
  t.string "name"
  t.integer "user_id"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["user_id", "name"], name: "index_reports_on_user_id_and_name", order: { name: :desc }
end

Here, the index for name has sort order in descending. Since the default is ascending, the sort order for user_id is not specified in schema.rb.

MySQL < 8.0.1

For MySQL < 8.0.1, running the above migration, would generate the following schema.rb

create_table "reports", force: :cascade do |t|
  t.string "name"
  t.integer "user_id"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["user_id", "name"], name: "index_reports_on_user_id_and_name"
end

As we can see, although the migration runs successfully, it ignores the sort order and the default ascending order is added.

Rails 5.2 and MySQL > 8.0.1

MySQL 8.0.1 added support for descending indices.

Rails community was quick to integrate it as well. So now in Rails 5.2, we can add descending indexes for MySQL databases.

Running the above migration would lead to the same output in schema.rb file as that of the postgres one.

create_table "reports", force: :cascade do |t|
  t.string "name"
  t.integer "user_id"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["user_id", "name"], name: "index_reports_on_user_id_and_name", order: { name: :desc }
end

Ruby 2.5 adds Hash#transform_keys method

This blog is part of our Ruby 2.5 series. Ruby 2.5 was recently released.

Ruby 2.4 added Hash#transform_values method to transform values of the hash.

In Ruby 2.5, a similar method Hash#transform_keys is added for transforming keys of the hash.

>> h = { name: "John", email: "john@example.com" }
=> {:name=>"John", :email=>"john@example.com"}

>> h.transform_keys { |k| k.to_s }
=> {"name"=>"John", "email"=>"john@example.com"}

The bang sibling of this method, Hash#transform_keys! is also added which changes the hash in place.

These two methods are already present in Active Support from Rails and are natively supported in Ruby now.

Rails master is already supporting using the native methods if supported by the Ruby version.

Rails 5.2 allows passing request params to Action Mailer previews

This blog is part of our Rails 5.2 series.

Rails has inbuilt feature to preview the emails using Action Mailer previews.

A preview mailer can be setup as shown here.

class NotificationMailer < ApplicationMailer
  def notify(email: email, body: body)
    user = User.find_by(email: email)
    mail(to: user.email, body: body)
  end
end

class NotificationMailerPreview < ActionMailer::Preview
  def notify
    NotificationMailer.notify(email: User.first.email, body: "Hi there!")
  end
end

This will work as expected. But our email template is displayed differently based on user’s role. To test this, we need to update the notify method and then check the updated preview.

What if we could just pass the email in the preview URL.

http://localhost:3000/rails/mailers/notification/notify?email=superadmin@example.com

In Rails 5.2, we can pass the params directly in the URL and params will be available to the preview mailers.

Our code can be changed as follows to use the params.

class NotificationMailerPreview < ActionMailer::Preview
  def notify
    email =  params[:email] || User.first.email
    NotificationMailer.notify(email: email, body: "Hi there!")
  end
end

This allows us to test our mailers with dynamic input as per requirements.

Ruby 2.5 enumerable predicates accept pattern argument

This blog is part of our Ruby 2.5 series. Ruby 2.5 was recently released.

Ruby 2.5.0 was recently released.

Ruby has sequence predicates such as all?, none?, one? and any? which take a block and evaluate that by passing every element of the sequence to it.

if queries.any? { |sql| /LEFT OUTER JOIN/i =~ sql }
  logger.log "Left outer join detected"
end

Ruby 2.5 allows using a shorthand for this by passing a pattern argument. Internally case equality operator(===) is used against every element of the sequence and the pattern argument.

if queries.any?(/LEFT OUTER JOIN/i)
  logger.log "Left outer join detected"
end

# Translates to:

queries.any? { |sql| /LEFT OUTER JOIN/i === sql }

This allows us to write concise and shorthand expressions where block is only used for comparisons. This feature is applicable to all?, none?, one? and any? methods.

Similarities with Enumerable#grep

This feature is based on how Enumerable#grep works. grep returns an array of every element in the sequence for which the case equality operator(===) returns true by applying the pattern. In this case, the all? and friends return true or false.

There is a proposal to add it for select and reject as well.

Rails 5.2 adds bootsnap to the app to speed up boot time

This blog is part of our Rails 5.2 series.

Rails 5.2 beta 1 was recently released.

If we generate a new Rails app using Rails 5.2, we will see bootsnap gem in the Gemfile. bootsnap helps in reducing the boot time of the app by caching expensive computations.

In a new Rails 5.2 app, boot.rb will contain following content:

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)

require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.

if %w[s server c console].any? { |a| ARGV.include?(a) }
  puts "=> Booting Rails"
end

This sets up bootsnap to start in all environments. We can toggle it per environment as required.

This works out of the box and we don’t have do to anything for the new app.

If we are upgrading an older app which already has bootsnap, then we need to make sure that we are using bootsnap >= 1.1.0 because new Rails apps ship with that version constraint.

If the app doesn’t contain the bootsnap gem already then we will need to add it manually since rails app:update task adds the bootsnap/setup line to boot.rb regardless of its presence in the Gemfile.

Ruby 2.5 requires pp by default

This blog is part of our Ruby 2.5 series. Ruby 2.5 was recently released.

Ruby 2.5.0-preview1 was recently released.

Ruby allows pretty printing of objects using pp method.

Before Ruby 2.5, we had to require PP explicitly before using it. Even the official documentation states that “All examples assume you have loaded the PP class with require ‘pp’”.

>> months = %w(January February March)
=> ["January", "February", "March"]

>> pp months
NoMethodError: undefined method `pp' for main:Object
Did you mean?  p
	from (irb):5
	from /Users/prathamesh/.rbenv/versions/2.4.1/bin/irb:11:

>> require 'pp'
=> true

>> pp months
["January",
 "February",
 "March"]
=> ["January", "February", "March"]

In Ruby 2.5, we don’t need to require pp. It gets required by default. We can use it directly.

>> months = %w(January February March)
=> ["January", "February", "March"]

>> pp months
["January",
 "February",
 "March"]
=> ["January", "February", "March"]

This feature was added after Ruby 2.5.0 preview 1 was released, so it’s not present in the preview. It’s present in Ruby trunk.

Array#prepend and Array#append in Ruby 2.5

This blog is part of our Ruby 2.5 series. Ruby 2.5 was recently released.

Ruby has Array#unshift to prepend an element to the start of an array and Array#push to append an element to the end of an array.

The names of these methods are not very intuitive. Active Support from Rails already has aliases for the unshift and push methods , namely prepend and append.

In Ruby 2.5, these methods are added in the Ruby language itself.

>> a = ["hello"]
=> ["hello"]
>> a.append "world"
=> ["hello", "world"]
>> a.prepend "Hey"
=> ["Hey", "hello", "world"]
>>

They are implemented as aliases to the original unshift and push methods so there is no change in the behavior.