-
Notifications
You must be signed in to change notification settings - Fork 298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
async exception issue #1207
async exception issue #1207
Conversation
As of 9d07728, I've investigated and found something interesting: rawAcquireSqlConn
:: forall backend m
. (MonadReader backend m, BackendCompatible SqlBackend backend)
=> Maybe IsolationLevel -> m (Acquire backend)
rawAcquireSqlConn isolation = do
conn <- MonadReader.ask
let rawConn :: SqlBackend
rawConn = projectBackend conn
getter :: T.Text -> IO Statement
getter = getStmtConn rawConn
beginTransaction :: IO backend
beginTransaction = conn <$ connBegin rawConn getter isolation
finishTransaction :: backend -> ReleaseType -> IO ()
finishTransaction _ relType = case relType of
ReleaseExceptionWith e -> do
putStrLn $ "got a release exception: " <> show e
connRollback rawConn getter
putStrLn "rolled back transaction"
_ -> connCommit rawConn getter
return $ mkAcquireType beginTransaction finishTransaction Note the two
We get This is running with the non-threaded runtime. Compiling with
```
λ ~/Projects/persistent/ matt/test-async-exception-issue* stack exec -- conn-kill +RTS -N2
[Info] [main thread] creating table...
creating conn
NOTICE: relation "foo" already exists, skipping
[Info] [main thread] [ThreadId 7] start inserting 100000000 things
[Info] [main thread] [ThreadId 7] inside of thing
[Info] [main thread] [ThreadId 7] Thing #: 1000
[Info] [main thread] [ThreadId 7] Thing #: 2000
[Info] [main thread] [ThreadId 7] Thing #: 3000
[Info] [main thread] [ThreadId 7] Thing #: 4000
[Info] [main thread] [ThreadId 7] Thing #: 5000
[Info] [main thread] [ThreadId 7] Thing #: 6000
[Info] [main thread] [ThreadId 7] Thing #: 7000
[Info] [main thread] [ThreadId 7] Thing #: 8000
[Info] [main thread] [ThreadId 7] Thing #: 9000
[Info] [main thread] [ThreadId 7] Thing #: 10000
[Info] [main thread] [ThreadId 7] Thing #: 11000
[Info] [main thread] killed thread
[Info] [main thread] destroying resources
[Info] [main thread] pg_sleep
got a release exception: thread killed
close' called
statements finalized boss
connection closed, boss
conn-kill: libpq: failed (another command is already in progress
)
creating conn
[Info] [main thread] result: Right [Single {unSingle = Nothing}]
λ ~/Projects/persistent/ matt/test-async-exception-issue* stack exec -- conn-kill +RTS -N2
[Info] [main thread] creating table...
creating conn
NOTICE: relation "foo" already exists, skipping
[Info] [main thread] [ThreadId 7] start inserting 100000000 things
[Info] [main thread] [ThreadId 7] inside of thing
[Info] [main thread] [ThreadId 7] Thing #: 1000
[Info] [main thread] [ThreadId 7] Thing #: 2000
[Info] [main thread] [ThreadId 7] Thing #: 3000
[Info] [main thread] [ThreadId 7] Thing #: 4000
[Info] [main thread] [ThreadId 7] Thing #: 5000
[Info] [main thread] [ThreadId 7] Thing #: 6000
[Info] [main thread] [ThreadId 7] Thing #: 7000
[Info] [main thread] [ThreadId 7] Thing #: 8000
[Info] [main thread] [ThreadId 7] Thing #: 9000
[Info] [main thread] [ThreadId 7] Thing #: 10000
[Info] [main thread] [ThreadId 7] Thing #: 11000
[Info] [main thread] [ThreadId 7] Thing #: 12000
[Info] [main thread] killed thread
[Info] [main thread] destroying resources
[Info] [main thread] pg_sleep
got a release exception: thread killed
close' called
statements finalized boss
connection closed, boss
creating conn
conn-kill: libpq: failed (another command is already in progress
)
[Info] [main thread] result: Right [Single {unSingle = Nothing}]
λ ~/Projects/persistent/ matt/test-async-exception-issue* stack exec -- conn-kill +RTS -N2
[Info] [main thread] creating table...
creating conn
NOTICE: relation "foo" already exists, skipping
[Info] [main thread] [ThreadId 7] start inserting 100000000 things
[Info] [main thread] [ThreadId 7] inside of thing
[Info] [main thread] [ThreadId 7] Thing #: 1000
[Info] [main thread] [ThreadId 7] Thing #: 2000
[Info] [main thread] [ThreadId 7] Thing #: 3000
[Info] [main thread] [ThreadId 7] Thing #: 4000
[Info] [main thread] [ThreadId 7] Thing #: 5000
[Info] [main thread] [ThreadId 7] Thing #: 6000
[Info] [main thread] [ThreadId 7] Thing #: 7000
[Info] [main thread] [ThreadId 7] Thing #: 8000
[Info] [main thread] [ThreadId 7] Thing #: 9000
[Info] [main thread] [ThreadId 7] Thing #: 10000
[Info] [main thread] [ThreadId 7] Thing #: 11000
[Info] [main thread] killed thread
[Info] [main thread] destroying resources
[Info] [main thread] pg_sleep
got a release exception: thread killed
close' called
statements finalized boss
connection closed, boss
creating conn
conn-kill: libpq: failed (another command is already in progress
)
[Info] [main thread] result: Right [Single {unSingle = Nothing}]
```
With four threads?
```
λ ~/Projects/persistent/ matt/test-async-exception-issue* stack exec -- conn-kill +RTS -N4
[Info] [main thread] creating table...
creating conn
NOTICE: relation "foo" already exists, skipping
[Info] [main thread] [ThreadId 9] start inserting 100000000 things
[Info] [main thread] [ThreadId 9] inside of thing
[Info] [main thread] [ThreadId 9] Thing #: 1000
[Info] [main thread] [ThreadId 9] Thing #: 2000
[Info] [main thread] [ThreadId 9] Thing #: 3000
[Info] [main thread] [ThreadId 9] Thing #: 4000
[Info] [main thread] [ThreadId 9] Thing #: 5000
[Info] [main thread] [ThreadId 9] Thing #: 6000
[Info] [main thread] [ThreadId 9] Thing #: 7000
[Info] [main thread] [ThreadId 9] Thing #: 8000
[Info] [main thread] [ThreadId 9] Thing #: 9000
[Info] [main thread] [ThreadId 9] Thing #: 10000
[Info] [main thread] [ThreadId 9] Thing #: 11000
[Info] [main thread] killed thread
[Info] [main thread] destroying resources
[Info] [main thread] pg_sleep
got a release exception: thread killed
close' called
statements finalized boss
connection closed, boss
conn-kill: creating conn
libpq: failed (another command is already in progress
)
[Info] [main thread] result: Right [Single {unSingle = Nothing}]
λ ~/Projects/persistent/ matt/test-async-exception-issue* stack exec -- conn-kill +RTS -N4
[Info] [main thread] creating table...
creating conn
NOTICE: relation "foo" already exists, skipping
[Info] [main thread] [ThreadId 9] start inserting 100000000 things
[Info] [main thread] [ThreadId 9] inside of thing
[Info] [main thread] [ThreadId 9] Thing #: 1000
[Info] [main thread] [ThreadId 9] Thing #: 2000
[Info] [main thread] [ThreadId 9] Thing #: 3000
[Info] [main thread] [ThreadId 9] Thing #: 4000
[Info] [main thread] [ThreadId 9] Thing #: 5000
[Info] [main thread] [ThreadId 9] Thing #: 6000
[Info] [main thread] [ThreadId 9] Thing #: 7000
[Info] [main thread] [ThreadId 9] Thing #: 8000
[Info] [main thread] [ThreadId 9] Thing #: 9000
[Info] [main thread] [ThreadId 9] Thing #: 10000
[Info] [main thread] [ThreadId 9] Thing #: 11000
[Info] [main thread] killed thread
[Info] [main thread] destroying resources
[Info] [main thread] pg_sleep
got a release exception: thread killed
close' called
statements finalized boss
connection closed, boss
creating conn
conn-kill: libpq: failed (another command is already in progress
)
[Info] [main thread] result: Right [Single {unSingle = Nothing}]
λ ~/Projects/persistent/ matt/test-async-exception-issue* stack exec -- conn-kill +RTS -N4
[Info] [main thread] creating table...
creating conn
NOTICE: relation "foo" already exists, skipping
[Info] [main thread] [ThreadId 9] start inserting 100000000 things
[Info] [main thread] [ThreadId 9] inside of thing
[Info] [main thread] [ThreadId 9] Thing #: 1000
[Info] [main thread] [ThreadId 9] Thing #: 2000
[Info] [main thread] [ThreadId 9] Thing #: 3000
[Info] [main thread] [ThreadId 9] Thing #: 4000
[Info] [main thread] [ThreadId 9] Thing #: 5000
[Info] [main thread] [ThreadId 9] Thing #: 6000
[Info] [main thread] [ThreadId 9] Thing #: 7000
[Info] [main thread] [ThreadId 9] Thing #: 8000
[Info] [main thread] [ThreadId 9] Thing #: 9000
[Info] [main thread] [ThreadId 9] Thing #: 10000
[Info] [main thread] [ThreadId 9] Thing #: 11000
got a release exception: thread killed
[Info] [main thread] killed thread
[Info] [main thread] destroying resources
[Info] [main thread] pg_sleep
rolled back transaction
close' called
statements finalized boss
connection closed, boss
creating conn
[Info] [main thread] result: Right [Single {unSingle = Nothing}]
λ ~/Projects/persistent/ matt/test-async-exception-issue* stack exec -- conn-kill +RTS -N4
[Info] [main thread] creating table...
creating conn
NOTICE: relation "foo" already exists, skipping
[Info] [main thread] [ThreadId 9] start inserting 100000000 things
[Info] [main thread] [ThreadId 9] inside of thing
[Info] [main thread] [ThreadId 9] Thing #: 1000
[Info] [main thread] [ThreadId 9] Thing #: 2000
[Info] [main thread] [ThreadId 9] Thing #: 3000
[Info] [main thread] [ThreadId 9] Thing #: 4000
[Info] [main thread] [ThreadId 9] Thing #: 5000
[Info] [main thread] [ThreadId 9] Thing #: 6000
[Info] [main thread] [ThreadId 9] Thing #: 7000
[Info] [main thread] [ThreadId 9] Thing #: 8000
[Info] [main thread] [ThreadId 9] Thing #: 9000
[Info] [main thread] [ThreadId 9] Thing #: 10000
[Info] [main thread] [ThreadId 9] Thing #: 11000
[Info] [main thread] killed thread
[Info] [main thread] destroying resources
[Info] [main thread] pg_sleep
got a release exception: thread killed
close' called
statements finalized boss
connection closed, boss
creating conn
conn-kill: libpq: failed (another command is already in progress
)
[Info] [main thread] result: Right [Single {unSingle = Nothing}]
```
It's inconsistent - I've seen two runs where it didn't happen, and a few more where it did. 🤔 On a "successful" run, we have this log of events:
On a failed run, it looks like this:
So the exact sequence appears to matter. I'm noticing that we call In (Note to self: investigate |
So here's -- | Convert a 'Pool' into an 'Acquire'.
poolToAcquire :: Pool a -> Acquire a
poolToAcquire pool = fst <$> mkAcquireType getResource freeResource
where
getResource = takeResource pool
freeResource (resource, localPool) = \case
ReleaseException -> destroyResource pool localPool resource
_ -> putResource localPool resource The docs on
Which, uh, does sound like a problem! |
And, yeah, avoiding it solves the problem. runSqlPool
:: forall backend m a. (MonadUnliftIO m, BackendCompatible SqlBackend backend)
=> ReaderT backend m a -> Pool backend -> m a
runSqlPool r pconn =
-- with (acquireSqlConnFromPool pconn) $ runReaderT r
withRunInIO $ \runInIO ->
withResource pconn $ \backend ->
runInIO $ runReaderT r backend With this, I don't ever see the problem happening. However, this also doesn't attempt to rollback the transaction. So it's not the same, really. I added transaction support in |
Let's compare with :: MonadUnliftIO m
=> Acquire a
-> (a -> m b)
-> m b
with (Acquire f) g = withRunInIO $ \run -> E.mask $ \restore -> do
Allocated x free <- f restore
res <- restore (run (g x)) `E.onException` free ReleaseException
free ReleaseNormal
return res
withResource ::
(MonadBaseControl IO m)
=> Pool a -> (a -> m b) -> m b
withResource pool act = control $ \runInIO -> mask $ \restore -> do
(resource, local) <- takeResource pool
ret <- restore (runInIO (act resource)) `onException`
destroyResource pool local resource
putResource local resource
return ret This is damn near identitcal. So what's wrong? |
@codygman want to test this out on your codebase? i'm going to need to backport it probably too |
runLoggingT | ||
(logError $ T.pack $ "Error closing database connection in pool: " ++ show e) | ||
logFunc | ||
UE.throwIO e |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a somewhat semantic change here. Instead of swallowing the exception, we allow resource-pool
to handle it somehow.
@@ -294,7 +245,7 @@ withSqlConn | |||
=> (LogFunc -> IO backend) -> (backend -> m a) -> m a | |||
withSqlConn open f = do | |||
logFunc <- askLoggerIO | |||
withRunInIO $ \run -> bracket | |||
withRunInIO $ \run -> UE.bracket |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slightly semantic change - bringing it in line with UnliftIO.Exception
semantics should make things easier in the future.
:: forall backend m | ||
. (MonadReader (Pool backend) m, BackendCompatible SqlBackend backend) | ||
=> m (Acquire backend) | ||
unsafeAcquireSqlConnFromPool = MonadReader.asks poolToAcquire |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait... These things are definitely part of the public API. So I can't just remove them without pointing users to a better thing! Damn.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I'll deprecate them when I backport this fix.
-- - initialize, afterwards, and onException. | ||
-- | ||
-- @since 2.12.0.0 | ||
runSqlPoolWithHooks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shiny new functionality! should be equivalent in terms of power.
@merijn I think you wrote the original Acquire
-based stuff, do you mind trying this out? Specifically the runSqlPoolNoTransaction
stuff should be what you need.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is an extremely late reaction, but I finally got around to (trying to) bump my code to GHC 9.2 and 9.4.
runSqlPoolNoTransaction
is fundamentally incompatible in doing what I was using unsafeAcquire
for, as it incurs a MonadUnliftIO
constraint. The Acquire
interface (like the surviving acquireSqlConn
) only requires MonadResource
. There is no MonadUnliftIO
instance for ConduitT
, which means that runSqlPoolNoTransaction
is unusable in large parts of my code.
@codygman has told me on Slack that this fixed his problem! 😂 🎉 Going to merge these PRs once CI passes here, and then release the backported fixes. |
I know what's going on! The problem isn't acquire API I added in 2.10.5. The problem is that the code mixes I can't believe I didn't notice that before in @codygman's code. I opened an issue about this on |
I'm not sure that |
Before submitting your PR, check that you've:
@since
declarations to the HaddockAfter submitting your PR:
(unreleased)
on the Changelogtest initially thanks to @codygman
investigating #1199
maybe even fixes #1199