TIL: ActiveRecord transactions roll back on Ruby Thread abort

I learned that ActiveRecord would roll back transactions when inside an aborted thread. It’s implemented right here, in ActiveRecord’s connection_adapters/abstract/transaction.rb:

def within_new_transaction
  # ...
ensure
  # ...
  elsif Thread.current.status == "aborting" || (!completed && transaction.written)
    # The transaction is still open but the block returned earlier.
    #
    # The block could return early because of a timeout or because the thread is aborting,
    # so we are rolling back to make sure the timeout didn't caused the transaction to be
    # committed incompletely.
    rollback_transaction
  # ...
end

How did I end up here? I recently implemented a feature in GoodJob to track active processes in the database using ActiveRecord. These database queries occur in a background thread where I also use the same database connection for GoodJob’s Postgres LISTENing. It looks something like this (pseudocode):

Concurrent::Future.execute do
  process = Process.create
  loop { listen_for_notify }
ensure
  process&.destroy!
end

While working on one of my Rails projects that use GoodJob (https://dayoftheshirt.com), I noticed that Process records were not cleaned up as I expected running when GoodJob async inside of Rails Server/Puma locally and exiting. Inspecting the logs, I saw this:

TRANSACTION (0.1ms)  BEGIN
GoodJob::Process Destroy (0.3ms)  DELETE FROM "good_job_processes" WHERE "good_job_processes"."id" = $1  [["id", "320de861-4c84-4f8c-ba3e-2e08a8ef0469"]]
TRANSACTION (0.1ms)  ROLLBACK

Huh. I did some Googling and found some references to rollback behavior, and asked in the Rails Link Slack, but nobody knew. This caused me to dig in ActiveRecord, where I found the behavior implemented.

I was still confused because I had never seen this behavior in GoodJob when building out the feature. But after much head-scratching, I realized that I hadn’t followed GoodJob’s README instructions for integrating with Puma, which ensures that GoodJob is gracefully shutdown before Ruby aborts threads at exit:

# config/puma.rb

on_worker_shutdown do
  GoodJob.shutdown
end

MAIN_PID = Process.pid
at_exit do
  GoodJob.shutdown if Process.pid == MAIN_PID
end

Mystery solved.


Javascript is a management problem

I have been on board with low/no javascript for a long time for a variety of observed and experienced reasons, and grasping around for a succinct explanation. Baldur Bjarnason captures it in “The Single-Page-App Morality Play”:

The problem is management.

It is a management problem. Truly.

The Multi-Page-App forces the team to narrow the scope to a level they can handle. It puts a hard limit on their technological aspirations. Mandating a traditional Multi-Page-App under the auspices of performance, accessibility, or Search-Engine-Optimisation is a face-saving way to force the hand of management to be more realistic about what their teams can accomplish. When we can accomplish the same by advocating for a specific Single-Page-App toolkit or framework, that’s what most of us nominally on the ‘Anti’ side do. I regularly advocate for Svelte when I think the team can handle its long term implications in terms of complexity. (That might change as Svelte adds more features.)

The problem with Single-Page-App frameworks, even the ones like SvelteKit who could claim to be more Hybrid than just SPA, is that they are very, very eager to enable ‘scale’ of any sort. Features, app size, code complexity, integrations, etc. They are desperate to make sure that you can keep using their framework if you become a mythical ‘unicorn’ startup and your project grows into the next Facebook. So they put a lot of hard work into making sure that there is no upper limit to the scope of the app you can make with them.

Which, when they present it as ‘scale’, sounds like a good thing. But it’s absolutely a bad thing when you’re in an industry that’s as mismanaged as ours. We can’t handle complexity. Having no upper limit to it is extremely bad.


I'm burning dude... where do we go from here?

A powerful conversation from Reveal’s “Mississippi Goddam Chapter 6: Mississippi Justice” (46:10) between reporters Al Letson and Jonathan Jones:

Al Letson

After the interview, I was still thinking about everything Curley said. JJ and I walked back to our car mostly in silence, but on the ride back, we talked about it.

Okay, so, what did you think about the interview with Curley?

Jonathan Jones

It was good. It was slightly pessimistic at the end of it.

Al Letson

That’s kind of what I wanted to talk to you about though, is that like, I am just really tired of people asking Black folks to have hope, and to make people feel better about the American situation, when everything in our history tells us that that is not the case. And I especially am uncomfortable with it when we are asking people who are doing the work, like Curley Clark is out there doing the work to try to make this world a more just place. And I, in the work that I choose to do, I’m trying to make the world a better place in it. And in the work that you do, it’s the same reason. We’re both driven to the work that we do and the cases that we look at and pick, because we want this place to be more just for everybody. And so, but the idea that somehow or another, Black folks are supposed to have hope, and Black folks are not supposed to be pessimistic. It just feels yucky to me, because it’s like asking us to look at all the history of what’s happened in this country and kind of ignore it.

Jonathan Jones

No, but I don’t disagree with you. I think that’s a misunderstanding of what I was asking. I wasn’t fishing for Black folk to fight.

Al Letson

No, I don’t think you were. That’s not what I’m saying. I’m not saying that you were telling me…

Jonathan Jones

Well, let me just… I mean, I am thinking about, I want to inspire. I want to… I don’t want to be an activist, but I hope our journalism helps to make this better world. And in investigative reporting, we do a great job of finding bad guys. We don’t do a very good job about solutions.

Al Letson

I think we look at it in two different ways, in the sense of, we’re both shooting for the same goal. And I want to be really clear that I didn’t think that you were telling Curley Clark to work harder. I felt like you were asking Curley, and by extension me, why don’t we have hope, and where’s the hope? And my point is that the history of this country tells me, no, no, I can’t do that anymore.

And I would say that, I think it’s beautiful that you want to inspire people. I do. I love that about you. And in certain ways, I do want to do work that inspires people, but really at this point in my life and where I am, I don’t want to inspire people, I want to infuriate people. I want to make work that make people burn so hard that they feel like this injustice is wrong, and they want to go out, and they want to tear it all down.

And I know that we’re talking about the same thing, but we’re talking about it in different ways, right? I know that you want to do the exact same thing, that you want to inspire people for this, to this higher cause. And I think that that is useful and smart, but also, I think, just like in series that we’re working on, I couldn’t do this without you, and you couldn’t do this without me, and the inspiration is definitely necessary, but fuck, man, I got so much anger.

I got so much anger, and I’m so tired of hiding it, and I’m so tired of pretending that it’s not there, and I’m so tired of this idea that… This shit really chokes me up a little bit. I’m just, I’m sick of it, man. I’m sick of it, And it makes… I want to… I’m burning, dude. And that’s why I’m doing this work, is because I’m seeing how the world is, and it’s not just Mississippi. It’s all, it’s the entire country. You see how it’s happening, and I don’t have hope, but what I do have is this rage, and it’s burning in me. And that’s what I hope that we do. I hope that we people off so bad at that they create change from that fire.

Jonathan Jones

And that seems very real, and I completely affirm that. I think maybe I would have, maybe I should have phrased the question a little bit better, because that answer that you gave would have been the answer. I guess my point was, where do we go from here? It can be from anger and raw rage, and outrage at injustice. It’s just, where do we go from here? And I don’t want to have a… I’m not trying to do a hokey fairytale at all.

Al Letson

Right, right.

Jonathan Jones

And it’s… it’s hard.


Types of Corruption

An interesting breakdown of types of corruption by Yuen Yuen Ang on Freakonomics:

I propose a typology of four types of corruption divided along two dimensions. First, whether the corruption involves elites or non-elites. And second, whether the corruption involves theft or exchange. So this intersection creates, first of all, corruption with theft, which I divide into petty theft and grand theft. Petty theft would be like extortion — a police officer who just stops you and robs you of $200. Grand theft would be embezzlement. Nigeria would be a classic case, billions of dollars siphoned out of a country. And then I distinguish between two types of transactional corruption. The first is what I call speed money, which is bribes paid to low- or medium-level officials in order to overcome red tape or delays or harassment. And then I have a fourth category called access money, which is privileges paid to powerful officials, not because you want to overcome red tape, but because you want to buy special deals from them.

[…]

I use the analogy of drugs because we know that all drugs are harmful, but they harm in different ways. Petty theft and grand theft are like toxic drugs, where if you take this drug, it’s definitely going to damage your health, you get no benefit from it… Speed money are like painkillers, so they help you to relieve a headache by overcoming red tape, but they don’t help you grow muscles fast. They don’t help you to grow your business. And access money are the steroids of capitalism, and steroids, we know, help you grow muscle fast. They help you perform superhuman feats. But they come with serious side effects that accumulate over time, and they only erupt in the event of a meltdown.


Conflict in the matrix

From Apenwarr’s “What do executives do, anyway?”:

To paraphrase the book [High Output Management, by Andy Grove], the job of an executive is: to define and enforce culture and values for their whole organization, and to ratify good decisions.

That’s all.

Not to decide. Not to break ties. Not to set strategy. Not to be the expert on every, or any topic. Just to sit in the room while the right people make good decisions in alignment with their values. And if they do, to endorse it. And if they don’t, to send them back to try again.

There’s even an algorithm for this.

It seems too easy to be real. For any disagreement, identify the lead person on each side. Then, identify the lowest executive in the corporate hierarchy that both leads report into (in the extreme case, this is the CEO). Set up a meeting between the three of them. At the meeting, the two leads will present the one, correct decision that they have agreed upon. The executive will sit there, listen, and ratify it.

But… wait. If the decision is already made before the meeting, why do we need the meeting? Because the right decision might not happen without the existence of that meeting. The executive gives formal weight to a major decision. The executive holds the two disagreeing leads responsible: they must figure out not what’s best for them, but what’s best for the company. They can’t pull rank. They can’t cheat. They have to present their answer to a person who cares about both of their groups equally. And they want to look good, because that person is their boss! This puts a lot of pressure on people to do the right thing.

From HBR’s “Problems of Matrix Organizations”:

Another possible source of decision strangulation in matrix organizations occurs when managers frequently or constantly refer decisions up the dual chain of command. Seeing that one advantage of the conventional single chain of command is that two disagreeing peers can go to their shared boss for a resolution, managers unfamiliar with the matrix worry about this problem almost more than any other. They look at a matrix and realize that the nearest shared boss might be the CEO, who could be five or six echelons up. They realize that not too many problems can be pushed up to the CEO for resolution without creating the ultimate in information overload. So, they think, will not the inevitable disagreement lead to a tremendous pileup of unresolved conflict?

Certainly, this can happen in a malfunctioning matrix. Whether it does happen depends primarily on the depth of understanding that exists about required matrix behavior on the part of managers in the dual structure. Let us envision the following scene: a manager with two bosses gets sharply conflicting instructions from his product and his functional bosses. When he tries to reconcile his instructions without success, he quite properly asks for a session with his two bosses to resolve the matter. The three people meet, but the discussion bogs down, no resolution is reached, and neither boss gives way.

The two bosses then appeal the problem up a level to their respective superiors in each of the two chains of command. This is the critical step. If the two superiors properly understand matrix behavior, they will first ascertain whether the dispute reflects an unresolved broader policy issue. If it does not, they know their proper step is to teach their subordinates to resolve the problem themselves—not to solve it for them. In short, they would not let the unresolved problem escalate, but would force it back to the proper level for solution, and insist that the solution be found promptly.

Often, conflict cannot be resolved; it can, however, be managed, which it must be if the matrix is to work. Any other course of action would represent management’s failure to comprehend the essential nature of the design.

Lastly, Alex Komoroske’s deck on Slime Molds is good.


Behaving like the character in a story

I enjoyed reading Robert Kolker’s “Who Is the Bad Art Friend?”, specifically about the non-symmetric relationships of fiction and inspiration.

Larson’s biggest frustration with Dorland’s accusations was that they stole attention away from everything she’d been trying to accomplish with this story. “You haven’t asked me one question about the source of inspiration in my story that has to do with alcoholism, that has to do with the Chinese American experience. It’s extremely selective and untrue to pin a source of a story on just one thing. And this is what fiction writers know.” To ask if her story is about Dorland is, Larson argues, not only completely beside the point, but ridiculous. “I have no idea what Dawn is thinking. I don’t, and that’s not my job to know. All I can tell you about is how it prompted my imagination.”

When Larson discusses “The Kindest” now, the idea that it’s about a kidney donation at all seems almost irrelevant. If that hadn’t formed the story’s pretext, she believes, it would have been something else. “It’s like saying that ‘Moby-Dick’ is a book about whales,” she said. As for owing Dorland a heads-up about the use of that donation, Larson becomes more indignant, stating that no artist has any such responsibility. “If I walk past my neighbor and he’s planting petunias in the garden, and I think, Oh, it would be really interesting to include a character in my story who is planting petunias in the garden, do I have to go inform him because he’s my neighbor, especially if I’m still trying to figure out what it is I want to say in the story? I just couldn’t disagree more.”

“[Dorland] might behave like the character in my story,” [Larsen] said. “But that doesn’t mean that the character in my story is behaving like [Dorland].”

Captain Awkward also had memorable advice:

Creatively speaking, I think you can generally write whatever you want about whoever you want. You do not owe people flattering portrayals in your fiction and you don’t owe the world a fair hearing of both sides. Even if you’re writing memoir, the expectation is that you’ll write the truth about your own experiences and recollections of events. So, when it’s just you and a blank page that nobody else will see? Name names. Settle scores. Spill some beans. Layer in all the carefully hoarded observations and details you’ve been saving up. You don’t have to solve publishing problems right this second, so feed the fire in your belly with your ex’s pet name for himself if that’s what gets it done; spite is motivating. You can always pull up a name generator later when you need to.

Ethically speaking, being free to write whatever you want does not mean being free of consequences from what you wrote. If you include recognizable details about real people in your published writing, fictional or otherwise, if you disclose confidential information about them (like mental health diagnoses), assume that somebody will connect the dots, and assume that the people you wrote about will eventually feel some kind of way about it. They may not have grounds to sue you, but they might think you are a bad person or a lazy artist, and they might tell their own stories where you are not the hero.


TIL: Detecting block returns in Ruby

I was doing some research on introspecting Ruby on Rails database transactions for a Reddit thread, and came across this Rails PR that had some new Ruby behavior for me: detecting an early return from a block.

Some background: A Ruby language feature, that can frequently surprise people, is that using return within a Ruby block will return not only from the block itself, but also from the block’s caller too. Using next is really the only truly safe way to interrupt a block early; even break can be troublesome if the block is called by an enumerator. Also, next can take a return value too, just like return e.g. next my_value.

I found the Rails PR interesting, because it has a method for detecting and warning on an early return. Here’s a simplified example:

def some_method(&block)
  block.call
  completed = true # won't be called if the block returns early
ensure
  if completed
    puts "ok"
  else
    puts "returned early"
  end
end

some_method { return } # => "returned early"
some_method { next } # => "ok"

This works because the methods ensure block will always be called, even if #some_method returns early. That was a novel implementation for me.


Home buyer letter

Having now successfully purchased a home in San Francisco, I have had friends asked me for resources. This was the letter we sent to the seller, which is very SF, but also the energy to bring. I also learned that these letters are waning because they are biased af.

Dear [seller],

Your home at [street address] is the right fit for us, our two cats, and (hopefully) our future family. We love the long layout, high ceilings (we’re both tall) and sunny patio, and look forward to hosting friends for Sunday brunch someday.

We’re rooted in the city through the South End Rowing Club, St. Francis Lutheran Church, supporting the YMCA, kickstarting local businesses like Andytown coffee, and working at tech companies and nonprofits. Ben worked for three years just around the corner at California and Grant, and Angelina has worked at numerous tech companies around Union Square and SOMA. We love the quiet contrast of Nob Hill against the rest of downtown San Francisco.

We see SF as a long term home and [street address] as a homebase for work and play: swimming and rowing on the SF Bay, and wine trips up to Napa and Sonoma too. Your home would have the perfect commute for Ben, walking down the hill to his nonprofit job at [organization] where he builds technology to help people apply for food stamps, remove marijuana convictions from criminal records, and improve how government delivers services to those who need them most. And it’s a short walk for Angelina to the [company] Shuttle or Downtown SF [company] office where she does business development for maps and location technology in use around the world.

Your home would provide the greatest fit for us as we aim to continue to contribute to the fabric of the city, where we hope to raise a family and be active in the community. We aim to take great care of an already-loved property while advocating for a neighborhood that supports all members throughout the growth of our city.

Our offer is fair given the condition, amenities and comparable sales, and we aim for being the best buyer to steward this home and the neighborhood. We look forward to your consideration of our offer for [street address].

Sincerely,
Ben and Angelina


GoodJob, what's new: Cron, concurrency controls, and a dashboard demo (v1.12.0)

This is a quick roundup of what’s new with GoodJob v1.12.0 since the last update published for GoodJob v1.9.

GoodJob ( github ) is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails. If you’re new to GoodJob, read the introductory blog post.

For further details on the following updates, check out GoodJob’s Changelog or Readme.

Cron-like replacement for repeating/recurring jobs

GoodJob now ships with a cron-like replacement for repeating/recurring jobs. The cron-like process runs either via the CLI, or async within the web server process. Repeating jobs can be scheduled to the second, powered by the Fugit gem. Here’s what the configuration looks like:

# config/environments/application.rb or a specific environment e.g. production.rb

# Enable cron in this process; e.g. only run on the first Heroku worker process
config.good_job.enable_cron = ENV['DYNO'] == 'worker.1' # or `true` or via $GOOD_JOB_ENABLE_CRON

# Configure cron with a hash that has a unique key for each recurring job
config.good_job.cron = {
  # Every 15 minutes, enqueue `ExampleJob.set(priority: -10).perform_later(52, name: "Alice")`
  frequent_task: { # each recurring job must have a unique key
    cron: "*/15 * * * *", # cron-style scheduling format by fugit gem
    class: "ExampleJob", # reference the Job class with a string
    args: [42, { name: "Alice" }], # arguments to pass; can also be a proc e.g. `-> { { when: Time.now } }`
    set: { priority: -10 }, # additional ActiveJob properties; can also be a lambda/proc e.g. `-> { { priority: [1,2].sample } }`
    description: "Something helpful", # optional description that appears in Dashboard (coming soon!)
  },
  another_task: {
    cron: "0 0,12 * * *",
    class: "AnotherJob",
  },
  # etc.
}

Concurrency controls

GoodJob now offers an ActiveJob extension to provide customizable limits on the number of jobs enqueued or executed concurrently. Rails might upstream it too. Here’s how to configure it:

# app/jobs/my_job.rb
class MyJob < ApplicationJob
  include GoodJob::ActiveJobExtensions::Concurrency

  good_job_control_concurrency_with(
    # Maximum number of jobs with the concurrency key to be concurrently enqueued
    enqueue_limit: 2,

    # Maximum number of jobs with the concurrency key to be concurrently performed
    perform_limit: 1,

    # A unique key to be globally locked against.
    # Can be String or Lambda/Proc that is invoked in the context of the job.
    # Note: Arguments passed to #perform_later must be accessed through `arguments` method.
    key: -> { "Unique-#{arguments.first}" } #  MyJob.perform_later("Alice") => "Unique-Alice"
  )

  def perform(first_name)
    # do work
  end
end

Dashboard Demo

Don’t take my word for what a good job it is. Check out the new GoodJob Dashboard demo running on Heroku entirely within a single free dyno: https://goodjob-demo.herokuapp.com/

More news:

  • Wojciech Wnętrzak aka @morgoth became a GoodJob maintainer.
  • Wrote up details of the evolving development philosophy behind GoodJob.
  • The Dashboard now allows removing jobs, with more actions coming soon.

Contribute

Code, documentation, and curiosity-based contributions are welcome! Check out the GoodJob Backlog , comment on or open a Github Issue, or make a Pull Request.

I also have a GitHub Sponsors Profile if you’re able to support GoodJob and me monetarily. It helps me stay in touch and send you project updates too.


Paranoid and Reparative readings

From How Twitter can ruin a life: Isabel Fall’s sci-fi story “I Sexually Identify as an Attack Helicopter” drew the ire of the internet:

The delineation between paranoid and reparative readings originated in 1995, with influential critic Eve Kosofsky Sedgwick. A paranoid reading focuses on what’s wrong or problematic about a work of art. A reparative reading seeks out what might be nourishing or healing in a work of art, even if the work is flawed. Importantly, a reparative reading also tends to consider what might be nourishing or healing in a work of art for someone who isn’t the reader.