Short notes from engineering

Micro-posts on programming and patterns as we build Tramline in public. Follow this feed if you find these bits interesting.


2023 / Jan 10 — 14:58
kitallis

I have been using return a fair bit in transactions in Rails. In 7, these rollback the transaction and I use them as so.

They obviously require very careful writing and to remember that the return actually just doesn't jump out of the block, it cancels the transaction.

I prefer the fact that return from 8 will throw an exception, but I've found that using Github::Result around transactions as a pattern comes pretty close or is better (in some cases) already.

Consider this example code that creates and merges a pull request,

def create_and_merge!
  return Result.new(ok?: false) unless create.ok?
  upserted_pull_request =
    @new_pull_request.update_or_insert!(create.value)

  transaction do
    upserted_pull_request.close! # close the PR

    if merge.ok?
      Result.new(ok?: true)
    else
      return Result.new(ok?: false, error: "Failed!")
    end
  end
end

def create
  # creates a PR and returns a Result
end

def merge
  # merges a PR and returns a Result
end

Result = Struct.new(:ok?, :error, :value, keyword_init: true)

One problem here is that we're using custom Result objects which are not very chainable. But the other more shape-y problem is that we're having to check the output from merge, return an ok-Result or else cancel the transaction and then return a not-ok-Result. This not only feels like excessive work but also the use of return is unfortunate to essentially carry out a rollback + value type scenario.

With Github::Result we can rewrite it much more cleanly,

def create_and_merge!
   return GitHub::Result.new { raise CreateError } unless create.ok?
   upserted_pull_request =
     @new_pull_request.update_or_insert!(create.value!)

   GitHub::Result.new do
     transaction do
       upserted_pull_request.close! # close the PR
       merge.value!
     end
  end
end

As long as merge throws an exception, value! will raise it, rollback (throwing an exception will rollback) and opaquely pass it further up to the wrappper Result. This allows us to avoid return magic and ugly raises in the middle of the transaction block and chain the exceptions up.