Ruby is notoriously bad at concurrency, but usually we can limp along in concurrent environments with some combination of thread-safe best practices, ruby semaphores and database row locking. The latter is particularly effective for row updates. But what about reads?

Say I have a dangerous DeathStar class:

class DeathStar

  def fire_laser
    return if laser_fired?
    fire!
  end

  private

  def fire!
    # Fire protocol
    update(laser_fired: true)
  end
end

Let’s assume that we really don’t want to fire the laser more than once, because it costs a ton of Imperial Credit and Vader is strapped. But we have tons of information coming into the system indicating whether the laser should fire, and each one kicks off a redis-backed sidekiq process that runs on workers across multiple servers:

class IntelGatherer

  def new_planet_position(death_star_id)
    # Target planet positions are changing
    FireWhenReadyWorker.perform_async(death_star_id)
  end

  def laser_charge_state(death_star_id)
    # Charge state is changing
    FireWhenReadyWorker.perform_async(death_star_id)
  end
end
class FireWhenReadyWorker
  include Sidekiq::Worker

  def perform(id)
    death_star = DeathStar.find(id)
    death_star.fire_laser
  end
end

Given the size of the Empire, we’re getting tons of data about planets, fleets, etc. concurrently all the time, so the system is running the firing protocol more or less at the exact same time across multiple servers. This means if there is a delay between the laser_fired? check and the fire! completion then multiple background workers could make it passed our return if laser_fired? guard statement, and fire! will be called multiple times for the same DeathStar.

The solution to this issue isn’t jumping out at me immediately. It seems like you’d either need a row-level read lock on the database (which sounds weird, scary and unsupported), or some kind of redis-backed semaphore (concerning/complicated for similar reasons). Or is the problem here in the framing of the question, and this pattern shouldn’t even be used for this purpose? I’m curious to hear your thoughts!