Showing posts with label development. Show all posts
Showing posts with label development. Show all posts

Elephants, Goldfish, and assessing plans

The Elephants, Goldfish and the New Golden Age of Software Engineering is a software development approach created by Dave Rensin's team at Google. The basic idea is to prepare a comprehensive development plan within the "elephant" agent context, and then use many "goldfish" contexts to interrogate the plan. There is more depth to it than that, of course. What I find especially interesting is not just the approach, but also how much time you should expect to commit. Rensin writes that plans take about a week. This kind of explicit time commitment is rarely mentioned, but it seems right. This has been the norm for my entire career. You present an initial plan, discuss it with others, integrate feedback, present the next edition, perhaps divide one plan into multiple plans, and repeat. After a week of this, you have one or more plans that are far better than the initial one.

What Rensin does not address is how many people are involved. That is, is it just one human and one agent? I do not doubt that this pairing will create a robust plan with more rigor than we have created before. But is it a plan that should be implemented? A side effect of the previous approach is that multiple people have had a week to ponder the outcome. This leads to a better appreciation of whether or not the outcome is actually warranted. Those making the go/no-go decision are not required to make that decision quickly -- and they shouldn't. So, how will these plans be assessed in the context of the enterprise when few people have seen them and they appear fully baked?

Code quality and the disposable-durable axis

Everyone on LinkedIn, Hacker News, etc has an opinion about LLMs and code quality. None that I have read are anywhere near as grounded as Max Kanat-Alexander's answer is. In short, frame the answer along the disposable to durable axis. His presentation Does Code Quality Still Matter? It Depends on This is well worth the quarter hour of your time.

Classic Design Patterns: Where Are They Now

Brandon Rhodes' talk "Classic Design Patterns: Where Are They Now" is a good critique of the "Gang of 4" Design Patterns book that dominated software design thinking for decades. Why should you give your attention to a critique of a book from 1994? In part because the critique gives concrete examples of how successful design solutions become molded into programming languages and how programming languages outgrow the limitations that originally necessitated the design solution.

I don't know of the speaker, but he seems to speak at many Python conferences.

Update: I have since watched several of Brandon Rhodes' presentations. He is a good speaker and his presentations are worth your time.

Unsustainable advancement

Software developers have the bad habit of switching to programming languages and tools that result in applications consuming most of the resources from hardware advancements. The new 2025 laptop runs faster than the old 2015 laptop, but not nearly as fast as its components' raw improvements should provide. The switch from installed desktop applications to on-demand web applications has resource consumption that should rightly be called gluttonous. These switches have mostly burdened the user and not the developer. With the recent advent of vibe coding and agentic workflows these costs are now felt by the developer. And the costs are enormous. Collectively our datacenters consume the power output of whole electric generation facilities and the surrounding watersheds to cool them down. I don't see how this is a sustainable advancement; not for the profession and not for the planet.

Programming by coincidence

The more I think about vibe coding and see it in practice the more I am reminded of David Thomas' and Andrew Hunt's admonishment in The Pragmatic Programmer

We should avoid programming by coincidence -- relying on luck and accidental successes -- in favor of programming deliberately.

Why Stripe’s API Never Breaks

The short video Why Stripe’s API Never Breaks introduces how Stripe handles API versioning. Stripe's API URLs do not include a (consequential) version number and yet every API release is supported. Instead,

  1. The API client includes in each HTTP requests a header with the client's API release date, eg "X-Stripe-Version: 2024-01-23".
  2. Stripe's API server transforms the dated request into a current release request.
  3. Stripe's API server transforms the current release response into the dated response.

This does mean that each new API release needs transformers for all previous releases. I suspect that there are few breaking changes so most transformers default to the previous release's transformers.

Using a version HTTP header does not seem different than using a version number in the URL path, but there is something elegant about maintaining stable URLs across API versions. It also seems easier in most web frameworks (eg Rails) to introduce the transformers after routing.

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!

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.

A pair of formal diagram explanations. One for software and one for buildings.

The C4 set of diagrams are directly relevant to my work as a software engineer. Seeing the architecture profession's sets of diagrams is a useful reminder -- and an immediately obvious one for anyone who has owned a house! -- that different diagrams are needed for different purposes (ie, contexts).

The C4 Model – Misconceptions, Misuses & Mistakes • Simon Brown • GOTO 2024

What's in my set of architectural documents? Sharing everything: drawings, schedules, + specs.

Why I don't want color coded logs

Most developers laugh at me when I say I don't want color coded logs. They rarely ask why. Logs with color coded structures provide no actionable information. They actually obscure actionable information by forcing a distinction without a difference. What I am looking for are clues in the logs. Those clues are often easily overlooked small tokens. I use color or inversion to distinguish them so that their occurrence immediately stands out from the background noise. The highlight script is a simple tool for this.

Unnecessary frustration and toil

I spent a good part of yesterday tracking down a problem with the staging deployment of a feature I first started back in October of last year. (That it has taken this long to get it to staging has everything to do with how this organization manages work.) When you have such a extended period between implementation and deployment you rarely retain the feature's context and even rarer an environment within which to debug problems. It took me some time to regain that context and environment. (I should have left better notes for my future self.) Once I had that it became obvious that the feature worked and that the problem lay in the deployment.

The deployment is a small Kubernetes cluster. Each service has 2 or 3 container in several pods. I figured out the pod ids and container names (why are they all different!) and opened a terminal window for each and streamed the logs. I used a small script to highlight text that I was expecting to find. I then used the feature and discovered the problem was due to a corrupted private key stored in the deployment's database.

The organization uses Honeybadger.io to record exceptions and Elasticsearch to aggregate logs. These tools are intended to improve access to the details needed to debug issues. Each tool has its own user interface and mechanisms for accessing and searching. To use them you obviously need to understand these mechanisms and, more significantly, you need to know how the organization has configured them. That is, no two organizations use the same data model for how it records exceptions and log details. 

The developer needs documentation about the configuration and there was none. Well, that is not quite true. This organization has thousands of incomplete, unmaintained, and contradictory Confluence pages. The "information" available to the developer is actually worse than none at all as they will waste time trying to piece together some semblance of a coherent (partial) picture. What I eventually concluded was that it could not be done and my best path forward was to look at the raw container logs.

I understand that at this organization I am a contractor and so just developer meat. But what I have seen is that this global, financial, highly profitable organization does not do any better for their developer employees. Perhaps all industries are like this. I have only experienced the software development industry and here they are mostly the same. It makes me sad and mad to see and experience such unnecessary frustration and toil.

Transactions and some concurrency problems

A group of us are reading Kleppmann's Designing Data-Intensive Applications. Chapter 7 is on transactions and especially the different approaches used to address concurrency problems, ie the Isolation in ACID. What becomes clear is that transaction isolation levels can only mitigate some problems. It is your application's data model design and use that are mostly responsible for avoiding them. Here are the concurrency problems raised in this chapter:

Lost Updates. These occur when a process reads some records, modifies them, and writes them back. If the updated records had been modified by another process after the read then those updates would be lost.

Read Skew. This is a variation of Lost Updates due to the delays between steps in a multiple step operation. Processes A and B are interacting with the same records. Process A reads records X and Y (two steps). Process B updates records X and Y (two steps). Due to the delay between A's and B's steps, process A has the original X value but the updated Y value.

Write Skew. This occurs when process A reads some records to make a decision and then updates other records appropriate to the decision. While the decision is being made process B changes some records that would alter process A's decision. Process A is unaware of process B's changes and continues to make its updates which invalidates the data model.

Phantoms. This is a variation of Write Skew. Process A queries for records to make a decision. Process B inserts records that would have been included in process A's query results. Unaware of these inserts, process A makes its updates which invalidates the data model. The "phantoms" are the records not included in process A's query result.

Costs of helpful data flexibility.

I'm having a discussion currently with a young developer who has only ever worked in Ruby and JavaScript. I noticed that the developer had chained "symbolize_keys" to the end of a method call. In Ruby this converts a hash's keys from strings to symbols, ie { "a" => 123 } becomes { :a => 123 }. Their reason for this was to offer flexibility to the called as to how it returned the result. They thought this provided flexility and robustness. I countered that it did the exact opposite.

When a function can be given parameters and return results in multiple formats then robustness is only had when the function handles all formats equally. To do that the function needs to be tested with all formats. This can be done, but in practice, and I've seen across many organizations, it is not. Not only does the function need to be tested with the multiple formats, but the callers and the called need to be tested too. It's a combinatorial explosion of testing.

The other detriment to this flexibility is that since no function is sure of the format every function converts the data to its preferred format even if the data is already in the preferred format. This conversion adds to the function's code size and has a runtime cost (CPU and memory) on every invocation. The cost of a single use might be small, but our applications work in a world with thousands of concurrent sessions each with deep call chains, and expect microsecond responses. Those single uses add up.

My recommendation to the developer was to require one format as part of its contract and add validation that runs at least during testing. (I'd like to just tell them to use a typed language where this wouldn't even be an issue!)

I mentioned that the developer's experience is in Ruby and JavaScript. I have found that is common for such developers to not expect data to be in a specific format or type. I assume some of this comes from never being trained to always validate and convert data coming from the outside before using it inside. (Eg, directly passing around an HTML INPUT element's value or a database's column value.)  Once inside, you can be assured of its correctness. Instead, data is passed around without any function knowing a priori that it is correct.

I am unsure if I will convince this developer to not use "symbolize_keys". I am rowing against the tide.

Update: Not only did I not convince the developer, but the system's architect rejected it also.

SSL terminating tunnel using ghostunnel

From time to time the need for a simple SSL terminating tunnel is wanted. This is used to enable the browser to use an HTTPS connection to an HTTP server. It is common to use a proxy server, but I was curious if there was something simpler. I was able to create an SSL tunnel using ghostunnel

https://github.com/ghostunnel/ghostunnel

To build it for MacOS 14.7 I needed to update the go.mod to use toolchain go1.22.7 (instead of toolchain go1.22.4).

Created the cert and key

openssl req \
  -x509 \
  -newkey rsa:4096 \
  -keyout key.pem \
  -out cert.pem \
  -sha256 \
  -days 3650 \
  -nodes \
  -subj "/C=US/ST=RI/L=Providence/O=MojoTech/OU=Labs/CN=clientsite.com"

Add the client's domain name to /etc/hosts

127.0.0.1 clientsite.com

Run the tunnel

sudo ghostunnel server \
  --listen clientsite.com:443 \
  --target localhost:3000 \
  --cert cert.pem \
  --key key.pem \
  --disable-authentication

Run Python's file directory serving http server

python3 -m http.server 3000

And finally, open https://clientsite.com in the browser or with curl

curl -k https://clientsite.com

I think since this is Go and executables are statically linked, you could share the ghostunnel executable and PEMs with other developers.

A templating system using the file system for inheritance

Way back in the early days of the web, around 2004, I wrote a templating system that used the file system for inheritance. I think Fred Toth originally conceived of the technique. 

In the directory /A/B/C you place the template M with content

Hello [%include N%]

You then have the templating system expand /A/B/C/M. It would execute the directive [%include N%] to include the template N by looking up the directory tree, in order, /A/B/C/N, /A/B/N, and /A/N, and using the first N it found. You would place common templates (eg headers) and default content (eg company name) in the upper directories and "override" them in the lower directories. It worked really well for the mostly static sites my department was creating.

I have not seen something like this elsewhere. You can, however, achieve the same effect by manipulating your templating system's template search path per output document.

The system came to be called Trampoline and it has a Perl and a partial Java implementation. The implementations are in the Clownbike project at Source Forge. None of the templates Clownbike used made it to Source Forge, unfortunately. Those became the proprietary web sites our customers were paying for. Galley, an internal project, seems to have some.

I have no idea if any of this code still works. I am sure to be embarrassed by the code's quality! Some quiet, rainy day this winter perhaps I will try running it.

Remain in control of your runtime

Several years ago I wrote Remain in control of your search. When you directly provide to your users a mechanism that your application depends on for its runtime then you immediately lose control of your ability to evolve the application. The recent speaker on the Stack Overflow podcast spoke about how ScriptRunner was implemented in Groovy, and it directly provided Groovy as the user's scripting language. When the application's evolution required updating Groovy the developers incurred the burden of ensuring that all the user's Groovy scripts would continue to operate correctly as is. Scripting languages designed for embedding usually provide a sandbox mechanism for limiting access to runtime resources, but few (none?) limit the language itself. It would have been better if ScriptRunner's design used a Groovy-like language that they translated to the runtime language. Having done that they would have been free to update Groovy, incurring only the burden of ensuring the translation continued to work.

Maintenance, evolution, and technical debt are different

I was listening to a recent Stack Overflow podcast on technical debit and was getting progressively more annoyed. The cause of the annoyance was that the speaker was lumping all changes that were not feature changes as technical debit. The changes he was speaking about are better characterized as either maintenance, evolution, or technical debt.

Maintenance is bug fixes. The original implementation is deficient and needs to be repaired. This work is only related to the original design and current feature set.

Evolution is change needed due to alterations in the runtime environment. This work is only related to the original design and intended runtime environment. For example, you need to update a library and that results in a need to update the original implementation.

Technical debt comes about when the original design is unable to accommodate a new need. For whatever reason, it is no longer suitable.

Too often "technical debt" is used to mask an organization's lack of timely maintenance and planned evolution. Don't do that.

See also Technical Junk.

Why can't we validate a branch locally?

I have been on several projects where a developer's version control branch needs to pass continious integration (CI in CI/CD) on a remote service before it is eligible to be merged. The implication here is that a successful remote test run is sufficient validation for acceptance. But given that the remote CI environment is also unlike the production environment I have been wondering why do we accept this validation over a local validation?

It seems to me that an organization should strive to enable local validation over remote validation if only for its cost benefits. Central CI environments are costly to operate. A common consequence of this is that these costs are reduced by having several test runs share an environment. Tests fail not due to a developer’s negligence but to the missing isolation. Validation now requires multiple runs in the hope that the next one will avoid all the conflicts and pass. It is CI wack-a-mole style, a style that costs too much and delays feature releases.

All developer machines are capable enough to run CI in the background while development happens in the foreground. The only thing that is missing is a signifier that authenticates the successful run. Eg, a value tied to the code commit, the successful test run log commit, and the environment commit.

SQL Antipatterns: Avoiding the Pitfalls of Database Programming

SQL Antipatterns: Avoiding the Pitfalls of Database Programming is a useful compendium of common relation database design flaws. There are a number of anti-patterns where a single table is used to implement a data model that actually has more than one entity. Or, a metadata data model is implemented using a single table or multiple tables where, respectively, the reverse is better. The content is split into 5 parts: logical design, physical design, query, development, and appendixes. The first 3 are the most relevant to the book's title. The development chapter does have useful information on good application development practices, but could have been left out. Overall, worth reading and keeping on your shelf to share with others (and a reminder to yourself!) as the need arrises.

Clare Sudbery on what CI was intended to mean

I found this talk by Clare Sudbery on what continuous integration (CI) was intended to mean illuminating. In short, trunk based development, frequent commits, and all hands on deck when the tests aren't green. Her arguments are compelling, as are her critiques of PRs and the working environment they can engender. Unfortunately, it is not a practical methodology to use with legacy products that exhibit inconsistent architecture, poorly implemented, with spotty test coverage. Perhaps use feature branch development to address each of these failings so that you can have true CI in the future.

Is it a future I would enjoy? I don't think so. I can't help but think that trunk based development will further speed the assembly line that agile, esp Scrum, has already placed us all on.

Continuous Integration: That’s Not What They Meant • Clare Sudbery • GOTO 2023

ps I listen to this stuff on drives. Good talks generally don't need slides.