Welcome to My Blog.

Here, you will find posts, links, and more about code (primarily Ruby), business (bootstrapped SaaS), and a little of everything in between.

Building a Multi-Step Job with ActiveJob

Most Rails applications start pretty simple. Users enter data; data gets saved in the database.

Then, we move on to something a bit more complex. Eventually, we realized we should not do all the work in one request, and we started using some form of job to push work onto a background process.

Great. However, as complexity increases, we realize we do too much work in a single background job. So, the next logical option is to split the background job into multiple jobs. Easy enough, of course, but then we run into some gotchas:

  1. Do any jobs require other jobs to be completed first? And, of course, do any of those sub-jobs require other sub-jobs (and so on)
  2. How do we mentally keep track of what is going on? How do we make it easy for someone to jump into our code base and understand what is happening?

Over the years, KickoffLabs has processed billions of jobs. Breaking tasks down into small chunks has been one way we have managed to scale. One of the things I have found challenging over the years is keeping track of when/what is processed in the background jobs.

So, when I started to experiment with a new product idea, I wanted to find a way to tame this problem (and eventually roll it back into KickoffLabs, too).

I had seen Shopify's JobIteration library before but never had a chance to use it.

Meet Iteration, an extension for ActiveJob that makes your jobs interruptible and resumable, saving all progress that the job has made (aka checkpoint for jobs).

It recently popped up on my radar again, and I noticed it supports iterating over arrays. This gave me an idea. Typically, this library is used for iterating over a large number of items in a job and tracking your place. If the job restarts (or even raises an error), you can safely resume where you left off.

With that functionality alone, it is likely a quite helpful library for most projects. But what if we used it to define a series of steps a job needs to take? This way, we can have a single job that handles all of the processing for a necessary task.

If things can be run in parallel, one or more of the steps can create new child jobs as well.

With that in mind, here is "SteppedJob":

class SteppedJob < ApplicationJob
  include JobIteration::Iteration
  queue_as :default

  class_attribute :steps, default: []

  class << self
    def steps(*args)
      self.steps = args
    end
  end

  def build_enumerator(*, cursor:)
    raise ArgumentError, "No steps were defined" if steps.blank?
    raise ArgumentError, "Steps must be an array" unless steps.is_a?(Array)
    Rails.logger.info("Starting #{self.class.name} with at cursor #{steps[cursor || 0]}")
    enumerator_builder.array(steps, cursor:)
  end

  def each_iteration(step, *)
    Rails.logger.info("Running step #{step} for #{self.class.name}")
    send(step, *)
    Rails.logger.info("Completed step #{step} for #{self.class.name}")
  end
end

This could also be a module, but I have it set up as a base class.

To use it:

  1. Create a job that derives from SteppedJob
  2. Define an array of steps
  3. Add a method for each step

Here is a sample job. This job is enqueued like any other ActiveJob: ProcessRssContentJob.perform_later(content)

From there, each job step is executed, and the content argument is passed along to each step.

class ProcessRssContentJob < SteppedJob
  queue_as :default

  steps :format_content, :create_content_parts, :enhance_content_parts

  def format_content(content)
    content.text = BlogPostFormatter.call(content:)
    content.processing_status!
  end

  def create_content_parts(content)
    ContentPartsForContentService.call(content:)
  end

  def enhance_content_parts(content)
    EnhanceContentPartsService.call(content:)
  end
end
#

Minitest::Mock with Keyword Arguments

I had a small gotcha that derailed my afternoon. Hopefully, a Google search (or via our new AI overloads) will tell you if you hit the same.

First, here is the method I am trying to mock: identifier.call(transcript:)

My initial approach looked like this:

mock_identifier = Minitest::Mock.new
mock_identifier.expect :call, ["abc"], [{transcript: "some text"}]

But I kept getting an error like this: mocked method :call expects 1 arguments, got []

Eventually, via some debugging (binding.irb for the win), I found that I could call my mock like this:

identifier.call(:transcribe => "some text")

So that led me to believe something was getting confused with the mock + method signature.

I tried to double-splat the arguments but ended up with the same error.

mock_identifier = Minitest::Mock.new
mock_identifier.expect :call, ["abc"], [**{transcript: "some text"}]

Finally, I went with an alternative way to set and verify the arguments of the mock:

mock_identifier = Minitest::Mock.new
mock_identifier.expect :call, ["abc"] do |args|
   args == {transcript: "some text"}
end

And we are back in business...well, onto the next issue. But we are almost all ✅ now. 😀

#

Still Learning While Using AI To Code - Or, Enumerable#chunk_while

For someone who thoroughly enjoys the Ruby language, one of the more rewarding aspects of using tools like Cursor and Supermaven has been exposure to some interesting Ruby methods I have not seen before.

Today, it was Enumerable#chunk_while.

The docs say:

Creates an enumerator for each chunked elements. The beginnings of chunks are defined by the block. This method splits each chunk using adjacent elements, elt_before and elt_after, in the receiver enumerator. This method split chunks between elt_before and elt_after where the block returns false. The block is called the length of the receiver enumerator minus one

Yes, that meant nothing to me as well. But here is a sample that should help. Let's take a grocery list in JSON and group all the items by the aisle they are found.

# Sample grocery shopping list
shopping_list = [
  { aisle: 'Produce', item: 'Apples' },
  { aisle: 'Produce', item: 'Bananas' },
  { aisle: 'Dairy', item: 'Milk' },
  { aisle: 'Dairy', item: 'Cheese' },
  { aisle: 'Canned Goods', item: 'Chickpeas' },
  { aisle: 'Canned Goods', item: 'Tomato Sauce' },
  { aisle: 'Snacks', item: 'Chips' },
]

chunked_items = shopping_list.chunk_while do |item1, item2|
  item1[:aisle] == item2[:aisle]
end

grouped_items = chunked_items.map do |aisle_group|
  {
    aisle: aisle_group.first[:aisle],
    items: aisle_group.map { |item| item[:item] }
  }
end

In the end, we have an array that looks like this:

[
 {:aisle=>"Produce", :items=>["Apples", "Bananas"]}
 {:aisle=>"Dairy", :items=>["Milk", "Cheese"]}
 {:aisle=>"Canned Goods", :items=>["Chickpeas", "Tomato Sauce"]}
 {:aisle=>"Snacks", :items=>["Chips"]}
]
#

Ruby Map With Index

Ruby has a built-in helper on Enumerable called - each_with_index

# An array of fruits
fruits = ["Apple", "Banana", "Cherry", "Date"]

# Using each_with_index to print each fruit with its index
fruits.each_with_index do |fruit, index|
  puts "#{index}: #{fruit}"
end

Unfortunately, there is no equivalent for the Enumerable#map.

['a','b'].map_with_index {|item,index|} # => undefined method map_with_index'`

There are easy ways to work around this, but the cleanest is by chaining with_index to .map

fruits = ["Apple", "Banana", "Cherry", "Date"]

result = fruits.map.with_index do |fruit, index|
  "#{index}: #{fruit}"  # Format the output with index
end

Why? My guess is to keep the number of methods smaller over time. Technically each_with_index could be deprecated since Enumerable#each.with_index is available. This way, we do not need a _with_index for everything on Enermable.

(1..100)
   .select
   .with_index {|n, index| (n % 2 == 0) && (index % 5 == 0)}

# [6, 16, 26, 36, 46, 56, 66, 76, 86, 96]
#

AeroSpace Tile Manager

In the never-ending pursuit of optimal App/Window/Space management on my computer, I recently switched to AeroSpace

AeroSpace bills itself as:

an i3-like tiling window manager for macOS

If you are like me and have never used i3, you may be asking, what is this?

Honestly, it is hard to describe, but in a nutshell, it has replaced my usage of three different applications:

  1. OS X Spaces (grouping of windows/projects/etc)
  2. Magnet - window spacing
  3. Alt+Tab - intelligent, quick app switching

These three apps work as expected (and I have nothing but praise for Magnet, which always did its job), but I find I work in a much more consistent and repeatable environment and spend far less time navigating between apps.

I recommend checking out this video for a detailed overview.

#

Turbo Streams -- append_all

While adding revisions (auto-save and version history) to PhrontPage, I needed a way to add a new element to the page. Using turbo_stream#append, turbo_stream#update, etc. would work, but only if there is an element on the page with a known dom id. Eventually, I would like this item (a toast component) to be more generally available, so I did not want to need to have anything hardcoded on the page.

In addition to append, update, etc., there are matching methods append_all, update_all, etc., that allow targeting one or more items with more flexible query selectors.

Append to the targets in the dom identified with targets either the content passed in or a rendering result determined by the rendering keyword arguments, the content in the block, or the rendering of the content as a record append_all

In my case, the Toast is added to the page, displays, and then removes itself.

Hattip to Matt Swanson for highlighting the returning component for the turbo_stream response.

  def render_success_toast
    component = Admin::Toast::Component.new(text: "Your changes have been saved")
    render turbo_stream: turbo_stream.append_all("body", component)
  end

toast.gif

#

Kudos for the super helpful error message:

constraints

I have a constraint on the route, and I was passing "posts" instead of "posts.

delete "/orphans/:type/:uid", to: "orphans#delete", as: "delete_orphaned_items", constraints: {type: /(post|page)/}
#

Introduction to Minitest Mocks

Test doubles likes mocks and stubs can help us with isolating code under test with the rest of the system. Here’s how to mock in Minitest.

#

Returning from a JavaScript Loop with Some

I am working on the Revision1 functionality for PhrontPage (autosave + history). After getting the main pieces in place, I realized that you likely do not want revisions on new content until you reach a short minimal threshold (starting at 30 characters). So I added a quick check for the length of the markdown field, but that felt dirty hardcoding a specific field.

Next, I added a target(s) to my Stimulus controller. Now, any field with the target applied can be evaluated—there are no hardcoded field names, etc.

However, I could not for my life see why the following function was not returning true.

hasFieldsWithoutMinimalLength() {
  this.minimalLegthFieldTargets.forEach(field => {
    if (field.value.length < this.minimalLengthValue) {
      return true
    }
  })
  return false
}

In the golden age of programming (6 months or so ago), this would have resulted in a Google search or two, scrolling through Stack Overflow, or perhaps the X/Twitter hail mary. However, today, I can ask Claude via SuperMaven

(me) The function that starts on line #37, hasFieldsWithoutMinimalLength appears to iterate fine, but the return in line 40 is not stopping the function from returning false

And you get the following:

(Claude) You're correct in identifying the issue with the hasFieldsWithoutMinimalLength function. The problem is that the return true statement is inside the forEach loop, which doesn't actually stop the function's execution or return from the outer function. Instead, it only returns from the callback function passed to forEach.

I know there are folks generating whole applications, etc., but for me, this is a sweet spot. I enjoy writing code, the journey, and the act of creating2. Having AI there to point out my obvious or sometimes subtle bugs is an amazing boost to my productivity.

For those curious, here is the fix and why.

hasFieldsWithoutMinimalLength() {
  return this.minimalLegthFieldTargets.some(field =>
    field.value.length < this.minimalLengthValue
  )
}

The some() method of Array instances tests whether at least one element in the array passes the test implemented by the provided function. It returns true if, in the array, it finds an element for which the provided function returns true; otherwise, it returns false. It doesn't modify the array.

  1. Technically, I am babysitting an Elastic Search migration that has me rethinking my career choices

  2. No shade. You do you and find what/how/etc you enjoy. Shipping is awesome, and if generating code and ignoring it all makes that happen, go for it.

#