server: fix unexpected behaviour of waitForShutdown

With the current implementation, only the first call to `waitForShutdown` on a given
`ShutdownLatch` will return, while others will block (typically indefinitely). That's not
how one would expect a shutdown latch to work.

This isn't currently a concrete issue because we only wait once on each `ShutdownLatch`.
But in the context of #4154 we'll probably end up wanting to wait for shutdown from
multiple threads.

This adds a number of tests to verify the current behaviour, and adds a test for multiple
`waitForShutdown` calls that fails prior to the functional change.

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4162
GitOrigin-RevId: 9a108858d11390b847404f30bc7b93c06fc3f966
This commit is contained in:
Robert 2022-04-05 23:06:11 +02:00 committed by hasura-bot
parent d6d5c55c13
commit dfb72ecbad
4 changed files with 71 additions and 1 deletions

View File

@ -834,6 +834,7 @@ test-suite graphql-engine-tests
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
build-depends: build-depends:
aeson aeson
, async
, base , base
, bytestring , bytestring
, case-insensitive , case-insensitive
@ -898,6 +899,7 @@ test-suite graphql-engine-tests
Data.TimeSpec Data.TimeSpec
Data.TrieSpec Data.TrieSpec
Database.MSSQL.TransactionSpec Database.MSSQL.TransactionSpec
Hasura.AppSpec
Hasura.Backends.DataWrapper.API.V0Spec Hasura.Backends.DataWrapper.API.V0Spec
Hasura.Backends.DataWrapper.API.V0.ColumnSpec Hasura.Backends.DataWrapper.API.V0.ColumnSpec
Hasura.Backends.DataWrapper.API.V0.ExpressionSpec Hasura.Backends.DataWrapper.API.V0.ExpressionSpec

View File

@ -30,6 +30,7 @@ module Hasura.App
runHGEServer, runHGEServer,
setCatalogStateTx, setCatalogStateTx,
shutdownGracefully, shutdownGracefully,
waitForShutdown,
-- * Exported for testing -- * Exported for testing
mkHGEServer, mkHGEServer,
@ -504,7 +505,7 @@ newShutdownLatch = fmap ShutdownLatch C.newEmptyMVar
-- | Block the current thread, waiting on the latch. -- | Block the current thread, waiting on the latch.
waitForShutdown :: ShutdownLatch -> IO () waitForShutdown :: ShutdownLatch -> IO ()
waitForShutdown = C.takeMVar . unShutdownLatch waitForShutdown = C.readMVar . unShutdownLatch
-- | Initiate a graceful shutdown of the server associated with the provided -- | Initiate a graceful shutdown of the server associated with the provided
-- latch. -- latch.

View File

@ -0,0 +1,65 @@
{-# LANGUAGE NumericUnderscores #-}
module Hasura.AppSpec (spec) where
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async qualified as Async
import Control.Exception (throwIO)
import Hasura.App (newShutdownLatch, shutdownGracefully, waitForShutdown)
import Hasura.Prelude
import System.Timeout (timeout)
import Test.Hspec
spec :: Spec
spec = do
describe "ShutdownLatch" shutdownLatchSpec
shutdownLatchSpec :: Spec
shutdownLatchSpec = do
it "waitForShutdown blocks before shutdown, not after" $ do
latch <- newShutdownLatch
timeout 10_000 (waitForShutdown latch)
`shouldReturn` Nothing
timeout 10_000 (shutdownGracefully latch)
`shouldReturn` Just ()
timeout 10_000 (waitForShutdown latch)
`shouldReturn` Just ()
it "allows multiple calls to shutdownGracefully" $ do
latch <- newShutdownLatch
timeout 10_000 (waitForShutdown latch)
`shouldReturn` Nothing
timeout 10_000 (shutdownGracefully latch)
`shouldReturn` Just ()
timeout 10_000 (shutdownGracefully latch)
`shouldReturn` Just ()
timeout 10_000 (waitForShutdown latch)
`shouldReturn` Just ()
it "allows shutting down a thread" $ do
latch <- newShutdownLatch
Async.withAsync (waitForShutdown latch >> return ("shut down" :: String)) $ \async -> do
threadDelay 10_000
pollThrow async
`shouldReturn` Nothing
shutdownGracefully latch
(timeout 1_000_000 $ Async.wait async)
`shouldReturn` Just "shut down"
it "allows multiple threads to wait for shutdown" $ do
latch <- newShutdownLatch
timeout 10_000 (waitForShutdown latch)
`shouldReturn` Nothing
shutdownGracefully latch
timeout 10_000 (waitForShutdown latch)
`shouldReturn` Just ()
timeout 10_000 (waitForShutdown latch)
`shouldReturn` Just ()
pollThrow :: Async.Async a -> IO (Maybe a)
pollThrow async = do
res <- Async.poll async
case res of
Just (Left e) -> throwIO e
Just (Right x) -> pure $ Just x
Nothing -> pure Nothing

View File

@ -25,6 +25,7 @@ import Hasura.App
mkMSSQLSourceResolver, mkMSSQLSourceResolver,
mkPgSourceResolver, mkPgSourceResolver,
) )
import Hasura.AppSpec qualified as AppSpec
import Hasura.Backends.DataWrapper.API.V0Spec qualified as DataWrapper.API.V0Spec import Hasura.Backends.DataWrapper.API.V0Spec qualified as DataWrapper.API.V0Spec
import Hasura.Backends.MSSQL.ErrorSpec qualified as MSSQLErrorSpec import Hasura.Backends.MSSQL.ErrorSpec qualified as MSSQLErrorSpec
import Hasura.Backends.MySQL.DataLoader.ExecuteTests qualified as MySQLDataLoader import Hasura.Backends.MySQL.DataLoader.ExecuteTests qualified as MySQLDataLoader
@ -100,6 +101,7 @@ unitSpecs = do
describe "Data.Parser.URLTemplate" URLTemplate.spec describe "Data.Parser.URLTemplate" URLTemplate.spec
describe "Data.Time" TimeSpec.spec describe "Data.Time" TimeSpec.spec
describe "Data.Trie" TrieSpec.spec describe "Data.Trie" TrieSpec.spec
describe "Hasura.App" AppSpec.spec
describe "Hasura.Backends.DataWrapper.API.V0" DataWrapper.API.V0Spec.spec describe "Hasura.Backends.DataWrapper.API.V0" DataWrapper.API.V0Spec.spec
describe "Hasura.Backends.MSSQL.ErrorSpec" MSSQLErrorSpec.spec describe "Hasura.Backends.MSSQL.ErrorSpec" MSSQLErrorSpec.spec
describe "Hasura.Backends.MySQL.DataLoader.ExecuteTests" MySQLDataLoader.spec describe "Hasura.Backends.MySQL.DataLoader.ExecuteTests" MySQLDataLoader.spec