Discussion: "Notes on structured concurrency, or: Go statement considered harmful"

Thank you for the blog post, I really enjoyed reading it and thinking about what could be done with this idea.

One thing I thought of when you talked about not allowing background tasks to just go off, is some code in Discourse that wants to run a block of code after the request is done being rendered - that is, it wants to escape the nursery of the request! Oh no, that’s bad, and means we need an escape hatch around the nursery concept… right?

Well, let’s open the code and see what we can do about this.

def self.defer_track_visit(topic_id, ip, user_id, track_visit)
  Scheduler::Defer.later "Track Visit" do
    TopicViewItem.add(topic_id, ip, user_id)
    TopicUser.track_visit!(topic_id, user_id) if track_visit
  end
end

… huh, Scheduler::Defer.later sounds one heck of a lot like start_soon, except later rather than sooner, doesn’t it?

Aside: What’s this code doing? It’s logging some simple stats about GET requests. Waiting on a database write operation before you finish up a read is going to be really slow. So, we want to punt the database write until after the response has been fully served. The Deferred blocks run at the end of the request, right before returning to the listen/accept.

So all we really need to satisfy this use case is to escape the “HTTP request” context and lob a task onto the “HTTP server” context. This is solved by the server framework simply exposing a nursery somewhere that tasks like this can be tossed onto. In this case - a request just needs to expose an API to put callbacks in a list, and call them all after the request is done and before going onto the next one.

(Any truly long-lived tasks in Discourse go onto Sidekiq, where they get stored in Redis and picked up by background worker processes.)


You know, I hear the Go team has been struggling to come up with something that is truly worthy of calling something ‘Go 2.0’. May I propose this tagline:

Go 2 - ‘go’ less