Contributing to Ruby

This is a short tale about how I almost became a Ruby language contributor back in 2019. What's with the almost part? Well, technically, I did contribute to the Ruby Core, but the part I wanted to contribute to initially was extracted from the code and became a stand-alone gem, though still distributed with MRI Ruby.

Ruby is by far my favorite do-it-all programming language, with over a decade of professional use on the clock. And yes, Ruby + Rails is a killer combo even by today's standards. But what really bummed me out back in 2019 was the fact that while I've used hundreds of open-source gems, contributed to many and created a bunch of my own, I had literally zero contributions to Ruby itself. And I mean, it wasn't perfect, but the general expectation was that others were to fix any issues.

The problem

There are several classes in the Ruby codebase (stdlib) responsible for networking transport functionality, like Net::HTTP, Net::FTP and Net::SMTP. While they cover entirely different protocols, all of them have a rudimentary debugging mechanism built in (aka debug logger). Well, except for Net::FTP. And, coincidentally, I had to troubleshoot a bunch of protocol-specific issues in FTP services for one of my clients.

Let's have a look at debugger setup in Net::HTTP class:

require "net/http"

http = Net::HTTP.new(hostname)
http.set_debug_output $stderr
http.start { .... }

Similar happens in Net::SMTP:

require "net/smtp"

smtp = Net::SMTP.new(addr, port)
smtp.set_debug_output $stderr
smtp.start { .... }

With set_debug_output call, we are setting an internal @debug_output variable in each class. Depending on the protocol implementation, that roughly translates into:

@debug_output << msg + "\n" if @debug_output

When debugging is enabled, we're able to troubleshoot any issues without having to rely on monkey-patching of the transport classes and log all necessary debug messages to STDOUT/STDERR or a log file. Unfortunately, this does not apply to Net::FTP class. That's what I decided to fix.

A temporary solution I had to rely on was a monkey-patch in the project's codebase:

require "net/ftp"

class FTPWithLog < Net::FTP
  attr_accessor :debug_io

  def print(*args)
    (debug_io || STDOUT).write(args.join)
  end
end

Mostly because Net::FTP class had a debug_mode setting, but no way of specifying where the output should go (and thus no way of capturing it). FTP transport code is sprinkled with debug code like this:

if @debug_mode
  print "connect: ", host, ", ", port, "\n"
end

The solution

A straightforward fix came in a form of a PR to the Ruby Core. In fact, the said fix has been sitting in my branch since early 2019, but I didn't bother to formally submit it until later that year.

We're essentially adding the same debugging statement into the Net::FTP class for consistency reasons. Usage gist:

require "net/ftp"

conn = Net::FTP.new(...)
conn.debug_mode = true
conn.debug_output = STDERR

Due to various CI issues this PR was never merged, and has been marinating for like 2 years until one of the Ruby Core team member reached out and mentioned net/ftp library being extracted out of the core codebase. Perfect! I cut a new PR against ruby/net-ftp repo, and that work gets merged in a few weeks later.

Conclusion

My fixes in the Net::FTP class are nothing to really be proud of, in fact, those changes are pretty mundane. But the improvement is still an improvement, and a step forward, regardless of how small it is. I'm pretty sure someone out there had to rely on the same core class monkey-patching for troubleshooting. No more!

Changes are available in net-ftp/v0.2.0 release.

With that being said - don't be afraid to contribute back!