HN user

lirbank

68 karma

https://www.starmode.dev/ | https://www.lirbank.com/

Posts8
Comments24
View on HN

Right, that's a real concern with naive concurrent tests - you're at the mercy of timing and the test becomes flaky. That's exactly what the synchronization barrier solves: it forces both transactions to reach the critical point before either proceeds, so the race condition is guaranteed to occur on every run. No flakiness.

Fair concern about reaching inside systems - it's not something to do lightly. The hooks are designed to be minimal: production code never calls them, they only activate in tests. But the core point is narrower than the thread might suggest - the article isn't about whether to use atomic updates vs locks vs SERIALIZABLE. It's about when your code has operations that could race, how do you prove your handling actually works?

You're right, Postgres wraps every statement in an implicit transaction. The point of that first example is that the SELECT and UPDATE are in separate auto-committed transactions - there's no explicit transaction block wrapping both. So another connection can change the value between your SELECT and your UPDATE.

Oh that is super cool. Great prior art to study in combo with Loom. Very excited to dig in - imagine if there was an easy-to-use data race tester where you didn't have to figure out the interleaving points up front? Just point it at your code and let it find them. Exciting.

Yeah, the more I think about it, the more exciting this idea gets. The walkthrough in the article shows exactly why - I intentionally (to later show why that is wrong) place the barrier between the SELECT and UPDATE, which deadlocks instead of triggering the race. Getting the placement right requires reasoning about where the critical interleaving point is. An exhaustive approach would surface both outcomes automatically: this placement deadlocks, this one exposes the bug, this one passes. That would remove the hardest part of writing these tests.

Good call, SERIALIZABLE is a strong option - it eliminates a whole class of bugs at the isolation level. The trade-off is your app needs to handle serialization failures with retry logic, which introduces its own complexity. That retry logic itself needs testing, and barriers work for that too. On randomized testing - that actually has the same limitation you mentioned about barriers: you need to know where to point it. And without coordination, the odds of two operations overlapping at exactly the wrong moment are slim. You'd need enormous pressure to trigger the race reliably, and even then a passing run doesn't prove much. Barriers make the interleaving deterministic so a pass actually means something.

Absolutely - if you can express the whole operation as a single atomic statement, that's the best outcome. No locks needed, no race to test for. The article is about what comes next: when the logic can't collapse into one query, how do you verify your concurrency handling actually works?

Interesting! The barrier approach is more targeted: you specify the exact interleaving you want to test rather than exploring all of them. Trade-off is you need to know which interleavings matter, but you get deterministic tests that run against a real database instead of a simulated runtime. Exploring exhaustive interleaving testing against a real Postgres instance could be a fun follow-up - I'd be curious if it's practical.

Good point. The barrier pattern from the article applies to both approaches - whether you're using pessimistic locks or optimistic version checks, it's good to verify that the concurrency handling actually works. Barriers let you test that your version check correctly rejects the stale update, the same way they test that your lock prevents the race.

Here's a real-world example where atomic updates aren't an option - an order status transition that reads the current status from one table, validates the transition, and inserts into another:

await db().transaction(async (tx) => { await hooks?.onTxBegin?.();

  const [order] = await tx.select().from(orders)
    .where(eq(orders.id, input.id))
    .for("update");

  const [status] = await tx.select().from(orderStatuses)
    .where(eq(orderStatuses.orderId, input.id))
    .orderBy(desc(orderStatuses.createdAt))
    .limit(1);

  if (input.status === status.code)
    throw new Error("Status already set");

  await tx.insert(orderStatuses).values({ ... });
});

You need the transaction + SELECT FOR UPDATE because the validation depends on current state, and two concurrent requests could both pass the duplicate check. The hooks parameter is the barrier injection point from the article - that's how you test that the lock actually prevents the race.

Fair point - atomic updates like SET salary = salary + 500 sidestep the race condition entirely for simple cases. The examples are intentionally simplified to isolate the concurrency behavior. The barrier pattern is more relevant when you have read-modify-write operations that involve application logic between the read and the write - those can't always collapse into a single UPDATE.

Agree. This is great though. Don't need a separate service to email to quickly email a bunch of ppl. Avoid building products around a closed platform.

I am taking a stab on bookkeeping software for devs running a business. But I am thinking "Hacker Worthy" could be it's own category of software. What other types of products needs to be redone hacker worthy?

Every time I do bookkeeping for our family business, I wish the bookkeeping software was more like VSCode, so I started building that. A bookkeeping app for small businesses that looks and feels like a code editor. And then I kept building... Is this a terrible idea?

I really don't like boilerplates. They're always outdated and usually include more than what I want. I also find it interesting to set up the same tools every few months so I can see what they new defaults are etc.

But lately I've found myself copying a lot of boilerplate config between projects and I can't remember which project has the "winning" or "latest" configuration. So I figured, what the heck, maybe it is time for a template.

Do you also hate repo templates? Why? What would make them lovable?

He is a co-founder so I would believe he had more than a fraction of (unvested) stonks. A co-founder that left within a year, and prolly dropped all his shares (cliff, again my assumption).

Is this really a good thing though? Sounds a bit more like a soft landing than a success story. I am not sure this is in the interest for the framework either. Michael and Ryan will stay for two years until their lockin is over and then resign and start building the next thing (a good thing).