A job queue hands out work on leases. A worker takes a job and sends a heartbeat every minute to extend its lease. Now the worker freezes, say for a long garbage-collection pause. The lease expires, a second worker takes the job, and then the first worker wakes up with no idea any time has passed and writes its results anyway. What stops the two from clobbering each other?
Fencing. Each lease comes with a number that increases every time the job is handed out. Every write carries it, and the store rejects any write whose number is lower than one it has already seen. The woken worker's write bounces. The duplicate work still happens, but it can't corrupt anything.
That leaves a cost problem. If jobs are long and expensive, the stale worker may burn hours of compute before its write gets rejected. So add a check on the worker's side: the heartbeat already asks the queue to extend the lease, and if that request comes back rejected, the job is gone. The worker stops, frees its resources, and exits without writing a status, because the job isn't its job anymore.
Self-abort can't replace fencing. A frozen worker can't check anything, which is exactly the case that started this. And even a live worker has a window between "the check says I still hold the lease" and "my write lands." So each mechanism has its own job: fencing is the correctness guarantee, and self-abort is an optimization on top of it that saves money. You want both, but only fencing counts as the safety argument.
Fencing was the fix I was shown. The self-abort was my proposal, made because fencing alone still wastes the frozen worker's work; what I had to settle was that it's real practice but belongs in the cost column, not the safety one.
Martin Kleppmann's "How to do distributed locking" walks through the same GC-pause case with fencing tokens.