`closeRegistry` does not cancel a thread forked during close before it marks the registry closed

Aperta
#25 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

@dnadales ci sta già lavorando.

Dal 27/10/2026.

  • #27 di @dnadales — aperta

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
46/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
haskell
Ambito
backend

Direzione di ricerca

Start with unsafeReleaseAllBy in resource-registry/src/Control/ResourceRegistry.hs:680 and trace how registryReleaseThreads is read, how the registry is marked closed, and how linked threads are cancelled. Use the reproduction in the issue to check the late-fork race against the early control mode. Done means shutdown reliably cancels a thread forked during close without producing RegistryClosedException or ExceptionInLinkedThread.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

unsafeReleaseAllBy (resource-registry/src/Control/ResourceRegistry.hs:680) runs three steps:

  1. Read registryReleaseThreads and cancel those threads.
  2. Mark the registry closed.
  3. Release all resources that are left.

If a thread forks a new thread between step 1 and step 2, step 1 does not cancel the new thread.
The new thread then runs on a closed registry until step 3.
If it calls allocate in that time, it gets RegistryClosedException.

Example

withRegistry $ \rr -> do                          -- "main" thread
  _ <- forkThread rr "acceptor" $ forever $ do    -- "acceptor" thread
         conn <- accept
         forkLinkedThread rr "handler" $ do       -- one "handler" thread per connection
           buf <- allocate rr newBuffer freeBuffer
           serve conn buf
  waitForShutdown
-- when "main" leaves withRegistry, it calls closeRegistry

If a connection arrives during shutdown, the steps can run in this order:

main      step 1: read registryReleaseThreads = {acceptor}
acceptor  accept returns, forkLinkedThread -> handler
main      step 1: cancel acceptor
main      step 2: registry closed
handler   allocate rr ...  -> RegistryClosedException
main      step 3: cancel handler

The handler is linked, so "main" gets ExceptionInLinkedThread.
A normal shutdown then reports an error.

Reproduction

In each round, the "slow" thread is the newest thread, so step 1 cancels it first.
Its finally lets the acceptor fork the handler, which is after step 1 has read registryReleaseThreads.
In the control mode (early), the acceptor forks the handler before close starts.

{-# LANGUAGE ScopedTypeVariables #-}
import Control.Concurrent
import Control.Exception
import Control.Monad
import Control.ResourceRegistry
import Data.IORef
import System.Environment

-- Returns True if the handler got RegistryClosedException.
oneRound :: Bool -> IO Bool
oneRound late = do
  hit <- newIORef False
  go <- newEmptyMVar
  started <- newEmptyMVar
  withRegistry $ \rr -> do
    let handler = forever $ do
          r <- try (allocate rr (\_ -> pure ()) (\_ -> pure ()))
          case r of
            Left (_ :: RegistryClosedException) -> writeIORef hit True >> throwIO ThreadKilled
            Right (k, ()) -> void (release k) >> threadDelay 10
    _ <- forkThread rr "acceptor" $ do
      when late $ takeMVar go
      _ <- forkThread rr "handler" handler
      putMVar started ()
      waitForCancel
    _ <- forkThread rr "slow" $
      waitForCancel
        `finally` (when late (putMVar go ()) >> threadDelay 1000)
    unless late $ takeMVar started
    threadDelay 1000
  readIORef hit

-- Block until another thread cancels this one.
waitForCancel :: IO a
waitForCancel = forever (threadDelay 1000000)

main :: IO ()
main = do
  [mode, n] <- getArgs
  hits <- length . filter id <$> replicateM (read n) (oneRound (mode == "late"))
  putStrLn $ mode <> ": " <> show hits <> " of " <> n <> " rounds saw RegistryClosedException"

The handler loop calls threadDelay 10 because the body of a forkThread thread runs MaskedInterruptible.
Without a blocking call, the cancel from closeRegistry cannot reach the handler.

Built with GHC 9.10.3, -O1 -threaded, and run with +RTS -N4:

late: 4 of 1000 rounds saw RegistryClosedException
early: 0 of 1000 rounds saw RegistryClosedException

The count for late changes from run to run. In three runs it was 4, 14, and 18.

Lingua principale
Haskell
Stelle
2
Fork
0
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di IntersectMBO/io-classes-extra

Tutte le issue di IntersectMBO/io-classes-extra

Issue simili

Altre issue su Haskell

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.