Configuring System.Timeout.timeout for masked asynchronous exceptions
0 reputation · 24 Aug 2020, 18:51 UTC
0 reputation · 24 Aug 2020, 18:51 UTC
The goal is to ensure that System.Timeout.timeout can cancel an action even when the action masks asynchronous exceptions.
The current implementation relies on throwing ThreadKilled via throwTo to the worker thread; if the action uses mask or uninterruptibleMask, the exception is delayed until masking ends, allowing the action to exceed the time limit.
An open design question is whether the library should instead use a cooperative cancellation mechanism, such as an STM TVar, to guarantee timely interruption regardless of masking state.
What are the implications for existing code that depends on exception masking for resource safety? Would a cooperative approach introduce overhead or latency? Should the timeout function provide an option to choose between exception‑based and cooperative cancellation?
34775 reputation · 25 Aug 2020, 04:29 UTC
No exception-based timeout can cancel an action while it is inside uninterruptibleMask, and this is by design, not a bug. System.Timeout.timeout works by forking a delay thread that calls throwTo to deliver an exception to the worker. GHC only delivers asynchronous exceptions at points where the target thread is interruptible. Inside mask, delivery is deferred to the next interruptible point; inside uninterruptibleMask, even the throwing thread blocks until the mask region exits. So if your action wraps a long computation in uninterruptibleMask, the timeout fires only after that region ends — there is no configuration of timeout that changes this.
uninterruptibleMask is the foundation of resource safety in Haskell: bracket, finally, and withMVar rely on it to guarantee that cleanup actions (releasing locks, closing handles) run to completion even when an exception arrives mid-cleanup. If a timeout could preempt uninterruptible code, an exception landing inside a release action could leak a lock or leave an MVar permanently empty, producing deadlocks. Existing code that depends on masking is therefore not merely inconvenienced by the current behavior — it is protected by it. Any design that promises hard preemption of uninterruptible regions is unsound and should be rejected.
A cooperative scheme — a timer thread writing to a TVar, with the worker polling it — guarantees timely detection of expiry, not timely interruption. The worker still overruns until it reaches a check point. A long pure computation or a blocking FFI call that never polls will blow through the deadline exactly as it does today. The real costs are:
IO a the way timeout does. It is effectively a different function, not a mode of the same one.atomically read per iteration — but nonzero, and the checks must be threaded through the whole call tree.timeout as the default; document that actions needing hard deadlines must not hold uninterruptibleMask across long operations.timeout — the signatures genuinely differ.Confirm on your installed GHC/base version, since System.Timeout internals have varied across releases:
import Control.Concurrent (threadDelay)
import Control.Exception (uninterruptibleMask_)
import System.Timeout (timeout)
main :: IO ()
main = do
r <- timeout 1000000 $ uninterruptibleMask_ (threadDelay 3000000)
print r -- returns Nothing only after ~3s, not 1s
If this prints after roughly three seconds, masking is deferring delivery as expected. Also inspect the System.Timeout source in your installed base package to confirm the throwTo-based implementation, and test that bracket cleanup inside the masked action completes under whichever strategy you adopt.
Use comments to ask for clarification. Post a solution as an answer.
6,540 reputation · 24 Aug 2020, 20:29 UTC
The timeout thread that calls throwTo to deliver ThreadKilled will itself block if the target thread is inside uninterruptibleMask, because the runtime cannot deliver the exception until masking ends. This means the timeout thread does not continue to run or fire another timeout while waiting, so a long uninterruptible region not only delays exception delivery but also holds up the timeout thread.