`timeout` under `mask_`: a cancelled assassin's exception is delivered after the frame
Evaluación
Este issue todavía no se ha evaluado.
Descripción
Describe the bug
timeout is a scheduler primitive: a TimeoutFrame with a lock, and when the timer
fires with the lock still free, an assassin thread that takes the lock and
throwTos the TimeoutException to the timed thread. When the action finishes
first, the frame's exit sees the lock taken and kills the assassin
(atomically (takeTMVar lock) >>= killThread).
If the timed thread is masked (mask_) and runnable but not blocked when the
assassin's throwTo runs, the exception is queued on the thread (threadThrowTo)
and the assassin blocks. Killing the assassin raises ThreadKilled in it, but its
queued exception stays on the timed thread. The frame exits with Just x, and the
TimeoutException is raised when the mask ends, outside any TimeoutFrame, so it
unwinds to the top of the thread as <<timeout TimeoutId n>>.
GHC's System.Timeout.timeout has the same lock-and-assassin protocol, and base's
comment says why it kills the delivery thread: "killThread (or another throwTo) is
the only way to reliably interrupt a throwTo in flight". In the RTS, killing a
thread blocked in throwTo retracts the throw. io-sim mirrors the protocol but not
the retraction.
Reproduction:
A thread, masked, waits under a one-second timeout for a flag another thread sets
after a one-second delay, so the flag and the timeout fire in the same step. In
plain IOSim the writer thread runs first (woken by its timer), then the assassin
(forked at the same step and queued after it), then the timed thread, woken by the
write and now runnable but not blocked.
module Main (main) where
import Control.Concurrent.Class.MonadSTM (atomically, check, newTVarIO, readTVar, writeTVar)
import Control.Monad.Class.MonadFork (forkIO)
import Control.Monad.Class.MonadTest (exploreRaces)
import Control.Monad.Class.MonadThrow (mask_)
import Control.Monad.Class.MonadTimer.SI (threadDelay, timeout)
import Control.Monad.IOSim
import Data.Maybe (isJust)
sim :: Bool -> IOSim s Bool
sim masked = do
exploreRaces
flag <- newTVarIO False
_ <- forkIO $ threadDelay 1 >> atomically (writeTVar flag True)
(if masked then mask_ else id) $ do
r <- timeout 1 (atomically (readTVar flag >>= check))
return (isJust r)
main :: IO ()
main = do
print (traceResult False (runSimTrace (sim False)))
print (traceResult False (runSimTrace (sim True)))
Output:
Right False
Left (FailureException <<timeout TimeoutId 0 >>)
Unmasked, the exception lands inside the frame and the wait is reported as timed
out, which is fine at the boundary. Masked, the thread dies.
Where it bites
Any code that runs a timeout under mask_ and does not block between the timed
action's completion and the end of the mask. A blocking operation in between is
an interruptible point, where io-sim raises the queued exception inside the frame;
only a frame exit with no such point leaks.
A fix
At the TimeoutFrame exit, when the lock was already taken (v == False), the
thread's own queued TimeoutException tmid is stale: remove it from
threadThrowTo (and unblock its thrower) before continuing, in both
IOSim.Internal and IOSimPOR.Internal. Alternatively, when a thread blocked in
throwTo is itself killed, remove its pending entry from its target's queue, which
is what the RTS does.
Desktop (please complete the following information):
io-simversion: io-sim 1.8.0.1/main both plain and POR.
Additional context
Found by claude during review of https://github.com/IntersectMBO/ouroboros-network/pull/5462/
- Lenguaje dominante
- Haskell
- Estrellas
- 57
- Forks
- 25
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de IntersectMBO/io-sim
-
bug
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
IntersectMBO/io-sim#271 · 3 comentarios ·
-
Implement a workaround for "Overwhelming the TimerManager"Quizá libre de nuevo @coot la tomó hace 32 días y no hay ningún pull request abierto. Abiertoenhancement
IntersectMBO/io-sim#268 · 1 comentario · 1 asignado ·
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 55/100
IntersectMBO/io-sim#258 ·
-
bug
Dificultad 1/5 Menos de una hora Aptitud para principiantes 55/100
IntersectMBO/io-sim#248 · 1 comentario ·
-
SimEvent redesignAbierto
Dificultad 5/5 Más de una semana Aptitud para principiantes 35/100
IntersectMBO/io-sim#244 · 1 comentario ·
Todos los issues de IntersectMBO/io-sim
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Los mantenedores suelen responder en 1 día
-
Bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 67/100
Los mantenedores suelen responder en 1 día
-
pkg:crucible-mir type:bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
-
Heroes in statisticsAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100