Railway modeling in the mid to late 1970s

The "From the Archive" section of the most recent issue of Railway Modeller magazine (July 2025) referenced a cottage thatching article by Allan Downes in the July 1975 issue. This brought on a flood of memories from that period when I was building my GWR branch line in N guage. His work was wonderfully inspiring even with only black & white photos along side his hand-drawn diagrams and plans. Fortunately, Railway Modeller has all of their back issues online and so I was able to read that article and an earlier one (February 1975) where he introduced the thatching technique.

In hindsight, what I really miss about this time and Downes's approach was the simplicity of it all. I knew no different. I built a whole railway station, goods depot, engine yard, and town using nothing but what a 14 year old could find and afford. The most expensive material I bought were the sheets of printed brick from the Totnes hobby shop!

I have a few, blurry photos of my layout. Saddly, I had to leave it all behind when my family immigrated to the US.

Sidekiq and exclusive workers

In certain scenarios, a Sidekiq worker requires exclusive operation or access to specific data. While it is ideal for a worker to be designed as idempotent, achieving this can be challenging, particularly in a distributed system. For instance, consider a worker responsible for sending out emails. If two instances of this worker are running simultaneously, there is a high likelihood that recipients will receive duplicate emails. Even worse, these emails might contain slightly different content, leading to user confusion. To address this issue, Sidekiq Enterprise offers mechanisms that facilitate exclusivity (and rate limiting).

Note: I wrote this to help a client focus on a few methods of worker exclusivity that avoid race-conditions.

There are three common exclusivity needs:

  1. Exclusivity irrespective of the worker’s parameters.
  2. Exclusivity with respect to all of the worker’s parameters.
  3. Exclusivity with respect to some of the worker’s parameters.

There is a fourth exclusivity need, but handling this need is outside the scope of this post.

  1. Exclusivity with respect to model data.

Using Sidekiq options

The simplest method to achieve exclusivity #2 is to utilize Sidekiq’s sidekiq_options unique_for: duration. For example,

class UniqueExampleWorker
  include Sidekiq::Worker
 
  sidekiq_options unique_for: 1.minute
 
  def perform(params = {})
    # do the work
  end
end

The option’s duration represents an estimate of how long the worker is expected to run. This exclusivity will only be maintained for the specified duration. If the worker exceeds this time, Sidekiq will not prevent a second worker from running concurrently. When utilizing this mechanism, it's advisable to adopt a conservative approach regarding the duration; in this case, opting for a longer duration is preferable to aiming for precision.

If the worker finishes before the lock_timeout duration, the lock is released and another worker can be queued.

An additional benefit of utilizing this option is that if a worker is already queued or currently running, attempting to queue another worker (eg UniqueExampleWorker.perform_async) will return nil instead of the job id.

Using Sidekiq Limiter

A more adaptable approach to achieving exclusivity for #1, #2, and #3 is to utilize Sidekiq::Limiter.concurrent. Unlike the standard Sidekiq options, this approach is implemented within the worker's perform method. The worker operates as usual, while the limiter allows for a swift exit when necessary. For example, to configure and implement exclusivity #1

class UniqueClassExampleWorker
  include Sidekiq::Worker
 
  LIMITER = Sidekiq::Limiter.concurrent(
    "unique_class_example_worker_limiter", # name
    1, # count, ie expect exclusivity
    lock_timeout: 1.minute, # expected duration of worker
    wait_timeout: 0, # don't wait around for running worker to finish
    policy: :ignore, # don't reschedule the worker for later
  )
 
  def perform(params = {})
    LIMITER.within_limit do
      # do the work
    end
  end
end

The key to exclusivity is the limiter name and the count. Here the name is based on the class name. Any concurrent use of this worker, irrespective of parameters, will not enter the within_limit block.

The limiter’s lock_timeout represents an estimate of how long the worker is expected to run. This exclusivity will only be maintained for the specified duration. If the worker exceeds this time, Sidekiq will not prevent a second worker from running concurrently. It is advisable to adopt a conservative approach regarding the duration; in this case, opting for a longer duration is preferable to aiming for precision.

Unlike when using sidekiq_options, there is no easy way to know if there is an existing worker queued or running. It is possible to examine the Sidekiq queues for the worker, but without careful use of mutual exclusion this will result in a race-condition.

Testing

When testing a worker that uses this approach you will need to replace the concurrent limiter to avoid it’s interference.

require "spec_helper"
require "sidekiq/testing"
 
RSpec.describe UniqueClassExampleWorker do
  # ...
  before do
    stub_const("#{described_class.name}::LIMITER", Sidekiq::Limiter.unlimited)
  end
  # ...
end

Exclusivity with regard to parameters

If the worker's exclusivity must be restricted by one or more parameters (or even derived data), then utilize those values to create a unique limiter name. For example, to exclusively run one worker per id parameter

class UniqueParamsExampleWorker
  include Sidekiq::Worker
 
  def perform(params = {})
    limiter = Sidekiq::Limiter.concurrent(
      "unique_params_example_worker_limiter_#{params['id']}", 
      1, 
      lock_timeout: 1.minute,
      wait_timeout: 0,
      policy: :ignore,
    )
    limiter.within_limit do
      # do the work
    end
  end
end

This example creates the limiter within the perform method. The cost of using a limiter is not with its creation (it is just a plain-old-ruby-object), but with the execution of within_limit. It is only then that the Redis implementation of the limiter occurs.

A more general implementation is

class UniqueParamsExampleWorker
  include Sidekiq::Worker
 
  LIMITER_DEFAULT_OPTIONS = {
    lock_timeout: 1.minute,
    wait_timeout: 0,
    policy: :ignore,
  }.freeze
 
  def limiter(*ids, count: 1, **options)
    digest = ids.
      append(self.class.name).
      map(&:to_s).
      reduce(Digest::MD5.new, :update).
      hexdigest
    Sidekiq::Limiter.concurrent(
      "limiter_#{digest}", 
      count, 
      LIMITER_DEFAULT_OPTIONS.merge(options),
    )
  end
 
  def perform(params = {})
    limiter(params['id']).within_limit do
      # do the work
    end
  end
end

Testing

When testing a worker that uses this approach you will need to replace the concurrent limiter to avoid it’s interference.

require "spec_helper"
require "sidekiq/testing"
 
RSpec.describe UniqueParamsExampleWorker do
  # ...
  before do
    allow_any_instance_of(described_class).
      to receive(:limiter).and_return(Sidekiq::Limiter.unlimited)
  end
  # ...
end

Handling non-exclusive uses

The Sidekiq::Limiter.concurrent examples all utilize the policy: :ignore option. This option instructs Sidekiq to disregard non-exclusive uses. It overrides the default policy: :raise which triggers the Sidekiq::Limiter::OverLimit exception. When this exception is raised, Sidekiq will reschedule the worker for a later time. There is a corresponding backoff policy and a maximum rescheduling count associated with this rescheduling process.

However, you can use this to implement a non-exclusive handler. For example,

def perform(params = {})
  limiter(params['id'], policy: :raise).within_limit do
    # do the work
  end
rescue Sidekiq::Limiter::OverLimit
  # handle the over-limit
end

Just don’t re-raise the exception!

Hilary Gridley and building feedback tools

This morning YouTube added the following interview to my feed. Embarrassingly, I realized that I have spent all of my time understanding how to use AI as a coder or as a student/researcher. I had little understanding how other professionals use it every day. Hilary Gridley, the guest, showed a fascinating example of how she uses ChatGPT to build out a feedback tool (a custom "GPT") for herself and for her staff. The feedback is on slide decks, but it is essentially what my employeer is doing for PRs without all the custom coding. I found the whole interview fascinating and now want to see more of its kind.

How custom GPTs can make you a better manager | Hilary Gridley (Head of Core Product at Whoop)
https://www.youtube.com/watch?v=xDMkkOC-EhI

SAGA and AWI

After showing my dark age miniatures I decided that perhaps I would enjoy a solo game of SAGA. After buying a nice mat and setting up the game table I discovered I no longer had the faction dice. I had forgotten I had give them away. So I followed this advice and retrofitted a load of D6 dice and the Viking and Welsh faction boards.

I played a few games. I didn't really enjoy them. I'm not an experienced solo gamer and so that was a factor. What really bothered me was the scale, ie the sweep of the game. The 28mm figures and terrain on a 6'x4' table was visually congested, and the game play had no satisfying built-up or peak. Most faction units were in melee within a few turns and the outcome was obvious. It felt flat. Perhaps if I had been able to extract a story from the encounters and ensuing battles I would have enjoyed them more. (Bloodbaths are not stories.)

I have recently been reading Henry Hyde's Shot, Steel, and Stone rules, and listening to his podcast and that of the Yarkshire gamer. Their advocacy for the large wargame has definitely affected me. Next year is the 250th anniversary of the American War of Independence. The battles are smaller than Napoleon's so it seems practical to have enough figures and landscape to make it feel authentic and not gamey. Gaming the 165ish significant battles, some quite local, on a large table full of 10mm units seems wonderful.

Side-Effects are the Complexity Iceberg

I liked Kris Jenkins's talk Side-Effects are the Complexity Iceberg. Three takeaways for me were

  • His description of side-effect as "every function as two sets of inputs and two sets of outputs" nicely encapsulates the idea of parameters, results, and before and after states.
  • The urge to rewrite the system becomes more pressing as ever fewer people remain who understand why the system is as it is. That is, it is an institutional problem and not a technical problem.
  • If you really hate someone, teach them to recognize bad kerning.