Is `c:terminate/2` is guaranteed to be called when "a callback does one of the following"?

c:terminate/2 docs say:

c:terminate/2 is called (A) if the GenServer traps exits (using Process.flag/2)
and the parent process sends an exit signal, or (B) a callback (except c:init/1)
does one of the following:

  • returns a :stop tuple
  • raises (via Kernel.raise/2) or exits (via Kernel.exit/1)
  • returns an invalid value

(I’ve added (A) and (B) above.)

And this, in two paragraphs that follow:

If the GenServer receives an exit signal (that is not :normal) from any
process when it is not trapping exits it will exit abruptly with the same
reason and so not call c:terminate/2. Note that a process does NOT trap
exits by default and an exit signal is sent when a linked process exits or its
node is disconnected.

Therefore it is not guaranteed that c:terminate/2 is called when a GenServer
exits. […]

Does the “not guaranteed” part refer to only the first scenario, (A), when “the parent process sends an exit signal”? In other words, is c:terminate/2 guaranteed to be called in the second scenario, when “a callback (except c:init/1) does one of the following:”?

Related:

1 Like

I believe your reading is correct. That is to say: if your callback returns a :stop tuple, raises/exits, or returns an invalid value, terminate is guaranteed to be called.

However, if you are just implementing terminate and never do one of the above, there is not a strict guarantee that it will be called due to the reasons outlined.

2 Likes

Thanks Zach. If anyone else wants to also confirm (or refute) such reading, please do so.

1 Like

Iirc Terminate/2 is never guaranteed to be called. I believe it’s possible for there to be a race condition where something else slays your process using exit, :kill before it gets a chance to be called.

Are you trying to use terminate to guarantee cleanup of some resource or another?

3 Likes

Thanks Isaac for chiming in. (And also for the YouTube series.) Gotcha, so my OP presented a ‘wishful reading’, hoping that Therefore in the docs only applied to the first reason a terminate/2 gets called (a process sending an exit signal). Do you think it’d be worthwhile to expand the docs, and follow up the sentence

Therefore it is not guaranteed that c:terminate/2 is called when a GenServer
exits.

with

Note that c:terminate/2 is also not guaranteed to be called when a GenServer’s callback does one of the 3 listed things above.

Or maybe I just read too much into the docs in my desire to have such a guarantee on c:terminate/2, and the docs are already clear enough?

To avoid the XY problem: I’m trying to limit retries of ‘job requests’ by republishing the same RabbitMQ message, but with an incremented custom “retries” header. The need to cap retries is that some of the requests hit on invalid/garbage data, which causes handle_ callbacks to raise, and the message/request gets republished on the queue ad infinitum. The idea is to eventually drop such message (onto a DLQ) once the “retries” reaches a threshold.

I’ve sketched out a diagram below, with the current approach being summarised in the bottom-right blue text. I assume that even without a guarantee of c:terminate/2 being called, it should (?) get called often enough that the proposed retry-counter would eventually get incremented up to the threshold.

Another solution would probably be the top-left blue text, that is, having another process solely for counting identical messages/requests being made.

Pardon if this is not a helpful reply and not addressing your original concern but AFAIR Oban handles such scenarios quite well even on its free tier. Reference here: Oban error handling.