I have a very simple program. What it does is listen to a Postgres channel, when a notification arrives it does some work and then continues to listen again.
It can happen that a notification is somehow corrupted. Then the program should just crash. I have systemd setup to restart it and it is no big deal.
spawn do
begin
PG.connect_listen(ENV["POSTGRES"], "new_measurements") do |n|
server_id, file_system_id, measurement_id = n.payload.split(",")
capacity = pg.scalar("SELECT capacity FROM measurements WHERE id = $1", measurement_id).as(Int32)
if capacity > 90
pp "panic!"
end
end
ensure
pg.close
end
end
sleep
The exception is for instance thrown at the pg.scalar query. But I really want to catch all exceptions with an ensure and then crash/exit the program.
When an exception occurs now the program just hangs. It enters the ensure clause and then hangs on the sleep call? I don’t really know.
The spawn and sleep are necessary otherwise the program will exit immediately without processing notifications.
I’m pretty new to Crystal and I’m not sure what the canonical way of handling this is.
Without testing it myself I suspect something like a close wait channel will work for your usecase:
notification_errors = Channel(Exception).new
pg = PG.connect(...)
PG.connect_listen(...) do |n|
begin
capacity = pg.scalar(...)
raise "bad capacity" if capacity > 90
rescue e : Exception
notification_errors.send e
end
end
# This blocks the main fiber until something is send to the channel
e = notification_errors.receive
pg.close
abort "Loop broke: #{e.message}"
That was the missing piece in my understanding! An exception only crashes the fiber. That makes total sense.
I’ve modified my code according to your suggestion and that works like a charm. Clever using a channel like that.
As a cherry on the pie I learned about abort. Thank you, you’ve made my morning.
When it comes to desired behavior as opposed to current behavior, I agree that exceptions should bubble up (by default).
Changing so that exceptions bubble up would require a hierarchical structure of spawned fibers though, as otherwise there will be no guarantee that there is somewhere to bubble to. That hierarchy is basically issue #6468, which is no small task to implement.
That could also do away with having to bother with waiting on a channel, which would be a nice win.
Maybe crystal could add an option like ruby’s
Thread.abort_on_exception? Not sure if it would be useful but… typically you want to know when a thread dies unexpectedly? :)
Not globally. I suppose you can override Fiber#run to change that. But I don’t think that’s a good idea because there are fibers not initiated by your application.
Best way to deal with that is similar to what I showed in my previous comment: Rescue the exception in the spawned proc and handle it there.
Or wait for [RFC] Structured Concurrency · Issue #6468 · crystal-lang/crystal · GitHub