packages feed

shibuya-core-0.9.0.2: test-gc/Main.hs

{-# LANGUAGE OverloadedStrings #-}

-- This probe owns its process: retaining an AppHandle for cleanup would hide
-- the bug, and a weak pointer cannot guarantee cleanup after collection.
module Main (main) where

import Control.Concurrent (threadDelay)
import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar)
import Control.Exception (SomeException, displayException, try)
import Control.Monad (replicateM_)
import Effectful (liftIO, runEff)
import Shibuya.Adapter (Adapter (..))
import Shibuya.App
  ( AppConfig (..),
    ProcessorId (..),
    SupervisionStrategy (..),
    defaultAppConfig,
    mkProcessor,
    runApp,
    waitApp,
  )
import Shibuya.Core.Ack (AckDecision (..))
import Shibuya.Telemetry.Effect (runTracingNoop)
import Streamly.Data.Stream qualified as Stream
import System.Exit (die)
import System.Mem (performMajorGC)
import UnliftIO qualified as UIO

main :: IO ()
main = do
  started <- newEmptyMVar
  outcome <- try @SomeException $ UIO.timeout 5_000_000 $ UIO.race (gcWindow started) (worker started)
  case outcome of
    Left err -> die ("FAIL: bare waitApp died: " <> displayException err)
    Right Nothing -> die "FAIL: the GC observation window did not finish within five seconds"
    Right (Just (Right ())) -> die "FAIL: waitApp returned while the idle processor should still be running"
    Right (Just (Left ())) -> putStrLn "PASS: bare waitApp survives major collections"

gcWindow :: MVar () -> IO ()
gcWindow started = do
  -- Do not let a slow startup pass without exercising a running application.
  takeMVar started
  replicateM_ 5 $ do
    threadDelay 100_000
    performMajorGC
  -- Leave time for a linked exception from the last collection to arrive.
  threadDelay 200_000

worker :: MVar () -> IO ()
worker started = runEff $ runTracingNoop $ do
  -- A timer keeps the ingester alive just as a real adapter's poll delay does.
  -- An unreachable STM retry here would introduce a separate deadlock.
  let adapter =
        Adapter
          { adapterName = "gc-regression:idle",
            source = Stream.fromEffect $ liftIO $ do
              threadDelay 60_000_000
              fail "the idle adapter unexpectedly woke during the GC probe",
            shutdown = pure ()
          }
  result <-
    runApp
      defaultAppConfig {strategy = IgnoreFailures}
      [(ProcessorId "idle", mkProcessor adapter (\_ -> pure AckOk))]
  case result of
    Left err -> liftIO $ die ("runApp failed: " <> show err)
    Right app -> do
      liftIO $ putMVar started ()
      -- Deliberately no metrics server, retained handle, or subsequent stopApp.
      waitApp app