Space Vatican

Ramblings of a curious coder

Updating Dependabot PRs With Tapioca

I’ve been using sorbet recently on a project. If you haven’t come across it before, sorbet is a type checker for ruby that allows you to gradually add type annotations to your codebase (sigs in sorbet terminology) and then provide feedback (either on the command-line or in your editor) when things don’t check out.

As well as the sigs you add to your source, sorbet supports rbi files for describing your classes. In particular these are key for describing your dependencies. The recommended way is to use tapioca, a tool released by Shopify, for generating rbis. One of the things tapioca will do is autogenerate rbis for all of the gems you use. These autogenerated ones aren’t super smart (effectively just method name + number of arguments), but a lot better than nothing (there are also handwritten ones known as annotations - this is a small and nascent collection compared to typescript’s definitelytyped).

Of course, as your change or update your gems, the corresponding rbis need to be updated. If you run tapioca gem --verify then it will tell you whether there are any out of date gem rbis. We’ve added this to our CI to check that they’re always in sync. So far, so good! However we also use dependabot to manage our dependencies, so now every dependabot gem update PR would fail on CI, until someone came along and ran tapioca gem. This doesn’t take long (I wrote a shell alias to run bundler, run tapioca and then commit and push the results) but it’s an extra manual step and dependabot is all about removing manual steps.

AWS Lambda Layers and Ruby

I’ve been building a few things using lambda functions in ruby recently. Some of these are just standalone functions, some end up as more complicated stacks, with multiple functions working together, usually with a lot of common dependencies.

This was annoying to work with since on each sam build invocation bundler was redownloading & reinstalling these dependencies for each function, even if you had only changed one of the functions. By the time I got to 4-5 functions in a stack it was really starting to slow down that fast iteration that is so beneficial. This got me thinking - could I put the vendored bundle in a layer that was used by all my functions?

Brighton Ruby 2019

I was lucky enough to attend Brighton Ruby 2019 as a speaker this year, but that didn’t stop me from enjoying the other great talks. Maybe this summary will be useful when your boss asks you what you learned, which conferences videos to look forward to or just as a helping hand to remember a great day in Brighton.

Debugging a Memory Leak in a Rails App

I got a series of Rollbar alerts a few days ago: NoMemoryError: failed to allocate memory. The application wasn’t under any particular load at the time, and a quick look at memory usage stats in appsignal showed the telltale signs of a memory leak: slowly but inevitably rising memory usage.

Dynamic Filters With Capistrano 3

I’ve been updating some old deploy scripts from Capistrano 2 to Capistrano 3 recently. Needless to say, although superficially similar, a lot has changed under the hood (largely for the better. Capistrano 3 is in many ways a simpler, more predictable animal than its predecessor).

One change that bit me today is how filters work. In Capistrano 2, this was handled by environment variables such as HOSTS and ROLES. Capistrano 3 also add other ways of doing, such as via set :filter. The big difference is that Capistrano 3 creates its filter set once, whereas Capistrano 2 refiltered the hosts for a role everytime it fetched a list of hosts. This definitely makes things a lot simpler, but interferes with the tasks I have for doing rolling upgrades.

With Capistrano 2 I did this by updating the code on all the servers and then running the symlink and restart tasks on one host at a time, using HOSTS for each one. One way around it would be to just reimplement the symlink and restart tasks ourselves, call it in our own on block, and make on process the hosts in sequence. As ever though, I’m loath to reinvent the wheel.

A new concept in Capistrano 3 is that of custom filters, to allow you to expand on the HOSTS and ROLES filtering previously offered. This can be used with the task at hand: although the list of filters is static, Capistrano 3 does re-evaluate the list of servers against the filters with each call to roles. All that is needed is a filter that is reconfigurable. A filter is just an object with a public filter method. In all the examples an instance of a filter class is created wrapping the particular values that should be filtered, but it works just as well if the object passed to add_filter is the class itself:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class DynamicHostFilter

  def self.with_hosts hosts
    old = @hostnames

    common = [hosts, old].compact.reduce(:&)
    begin
      @hostnames = common
      yield
    ensure
      @hostnames = old
    end
  end

  def self.filter(servers)
    if @hostnames
      servers.select {|s| @hostnames.include?(s.hostname) }
    else
      servers
    end
  end
end

All this is a class with a class instance variable containing a list of hosts that it uses to filter servers. The with_hosts method allows us to set the filter for the duration of the block. Any new filter is intersected with existing filters and removed at the end of the block, which I think reduces the possibility for surprising behaviour. The rolling restart code is then just

1
2
3
4
5
  on( roles(:app), in: :sequence, wait: 5) do |host|
    DynamicHostFilter.with_hosts([host.hostname]) do
      #handle this host
    end
  end

which is actually a lot nicer than the old Capistrano 2 code it replaced.